Skip to content

Merge upstream firstmate into the fork - #67

Merged
cloud-practitioner merged 43 commits into
mainfrom
fm/fm-merge-upstream-12
Oct 25, 2026
Merged

cloud-practitioner merged 43 commits into
mainfrom
fm/fm-merge-upstream-12

Conversation

@cloud-practitioner

@cloud-practitioner cloud-practitioner commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Intent

fm-maint, prepare a pr getting updates from upstream

Context: one PR on cloud-practitioner/firstmate that brings everything new on kunchenguid/firstmate main since upstream 83b70ef (the upstream tip the previous merge-upstream, fork PR #62, merged) into fork main, the same way as that PR: a real merge of upstream main into a branch from current fork main, never GitHub "Sync fork", a rebase or a squash, with conflicts resolved so every fork-only feature and commit is kept.
Fork-only features that must survive include the PR evidence gate (fork PR #64), the durable stand-down and resume for local secondmates (fork PR #65), the live Claude launcher isolation (fork PR #63), the contrib/iqx-ixia-hook/ folder, the Bitbucket CI-ready recognition and named-head gating, the per-home task temp root, the Bitbucket PR checks, merge guards and merge-message option, and the other fork-only fixes already on fork main.
It must land as a merge commit, never a squash or rebase, so fork history stays intact, and the PR description says so.
Upstream is only fetched: nothing is pushed, opened, filed or commented on kunchenguid/firstmate; the PR is opened on cloud-practitioner/firstmate only, runs the fork's validation path, and is not merged as part of this work.
The report gives the PR URL, the upstream range merged, and any conflict that needed a judgment call, including any where keeping a fork feature meant dropping or reshaping an upstream change.
The PR description's risk assessment describes the final PR head, with no stale findings from before a fix.
When planned, upstream main was af1c681, 36 upstream PRs ahead of 83b70ef:
kunchenguid#7552, kunchenguid#7407, kunchenguid#7364, kunchenguid#7631, kunchenguid#7630, kunchenguid#7633, kunchenguid#7625, kunchenguid#6588, kunchenguid#7636, kunchenguid#7639, kunchenguid#7634, kunchenguid#7649, kunchenguid#5688, kunchenguid#7530, kunchenguid#7637, kunchenguid#6094, kunchenguid#7627, kunchenguid#7650, kunchenguid#7466, kunchenguid#7618, kunchenguid#7149, kunchenguid#6983, kunchenguid#5359, kunchenguid#7656, kunchenguid#7336, kunchenguid#7662, kunchenguid#7647, kunchenguid#6163, kunchenguid#7666, kunchenguid#5947, kunchenguid#6300, kunchenguid#7421, kunchenguid#6440, kunchenguid#6722, kunchenguid#6467, kunchenguid#7685.

What Changed

  • Bring in upstream kunchenguid/firstmate main (83b70ef4..af1c6817) through a real merge into the fork branch. This PR must land with a merge commit, never squash or rebase, to preserve fork history.
  • Add the experimental Rex backend, OpenCode 2 support, OpenRouter dispatch, opt-in PR health monitoring and Claude usage warnings, and approval-gated Codex/Claude managed-approval failover.
  • Improve watcher wake recovery and wedge controls, lane-capacity admission, session and remote-worker identity, held-call durability, and teardown safety; extend regression coverage and documentation, and expand portable CI to ten shards.

Merge method

This PR must land as a merge commit, never a squash or a rebase, so fork history stays intact.
The branch holds a real git merge --no-ff of upstream kunchenguid/firstmate main (af1c6817, the second parent) into fork main (244d8368, the first parent) as 02c9c410, followed by the follow-up commits listed below.
It is not GitHub "Sync fork", a rebase or a squash.
Upstream was only fetched by URL; nothing was pushed to, opened on, filed on or commented on kunchenguid/firstmate.

Upstream range merged

Range 83b70ef4..af1c6817 (the previous merge, fork PR #62, ended at 83b70ef4): 36 upstream commits, 36 upstream PRs, upstream tip unchanged since planning.

Conflicts and resolutions

27 files conflicted. Default was to keep both sides; where upstream shipped its own version of a fork change, upstream's implementation was taken.

  • .agents/skills/afk/SKILL.md, CONTRIBUTING.md: upstream's shorter backend and supervisor-pane wording (both sides rewrote the same line to the same anchor).
  • .agents/skills/harness-adapters/references/harness/kimi.md: the fork's one-sentence-per-line text, plus a line saying Rex is also in the viewport-capture set.
  • README.md: upstream's Rex-aware "Restart-proof" line and the fork-only "PR evidence" feature line.
  • bin/fm-backend.sh: fm_backend_agent_state keeps the fork's expected-label argument for herdr and dispatches rex to upstream's classifier.
  • bin/fm-config-inherit-lib.sh: upstream's inheritable list (adds codex-approval-mode) plus the fork's pr-evidence.
  • bin/fm-control-lib.sh, bin/fm-control.sh, bin/fm-spawn.sh, bin/fm-crew-state.sh: see judgment call 3; the spawn meta key list is upstream's plus the fork's herdr_process_identity.
  • bin/fm-remote-job-lib.sh, bin/fm-remote-job-worker.sh: upstream's whole implementation taken (superseded list below).
  • The held-call lookup script, its lifecycle doc and its test: upstream's implementation taken (judgment call 1).
  • bin/fm-test-run.sh: union of the pr-forge family list (fork Bitbucket suite, upstream PR-health suite).
  • docs/agent-control.md, docs/architecture.md, docs/scripts.md: both sides' additions merged (fork PR-identity, Herdr restart pointer, non-dormant liveness wording, evidence rows; upstream Rex wording and PR-health row).
  • docs/configuration.md: upstream's lane-capacity, PR-ready-after-green and task-status-archive sections and Rex backend lines.
  • docs/verification/runtime-backends.md: the fork's busy-inbox verification notes kept, because the fork's inbox grace replaced the stuck-busy escalation upstream's live-proof text describes; upstream's OpenCode 2 and OpenCode 1 sections kept.
  • tests/lib.sh: upstream's fixture-root process reaping kept; the fork's tree removal stays in tests/fixture-tree-helpers.sh; cleanup reaps, then removes spawn launch dirs, then the tree.
  • tests/fm-backend-herdr-presentation-e2e.test.sh: upstream's tolerant concurrent-recovery helper plus the fork's slow-lock-holder serialization assertions (real-Herdr suite, not run here).
  • tests/fm-backend-herdr.test.sh: the fork's stopped-server piped-reader test with upstream's fm_run_timed.
  • tests/fm-remote-secondmate-lifecycle-e2e.test.sh: the fork's larger wait bound.
  • tests/fm-supervision-host.test.sh: upstream's case list, each wrapped in the fork's per-case cleanup, plus the fork's downtime-write-failure case.
  • tests/fm-test-fixture-cleanup.test.sh: both case calls kept.

Judgment calls (including where a fork feature reshaped upstream)

  1. Held-call Done-archive lookup (fork PR 34 vs upstream fix(bin): count answered captain holds moved to the Done archive as durable kunchenguid/firstmate#6094). Both implemented the same feature; upstream's was taken whole. Running the fork's regression tests against it showed two fork behaviours it lacked, so they were re-applied on top of upstream's code (this reshapes upstream's function): the TOML reader treats a # inside a quoted value as data and accepts a spaced table header such as [ markdown ]; and durability is judged per identity, exact id first, so an earlier answer on a live legacy-derived row cannot vouch for a call whose own row was archived unanswered.
  2. Tenth serial test shard. The merged tree (283 tests) no longer packs into nine serial shards under the 1200000 ms budget (largest shard 1224902 ms). PORTABLE_SERIAL_SHARDS is now 10 in bin/fm-test-run.sh, with the matching shard matrix in .github/workflows/ci.yml, the required-context list in CONTRIBUTING.md and one doc sentence. Operator action: add Behavior portable serial 10 to any branch-protection required-check list that names serial 1 to 9.
  3. fm_control_endpoint_absence_verdict third argument is backend specific: herdr takes the fork's expected pane label, rex takes the task meta (upstream's rex proof reads rex_server from it). Callers in fm-control exit and fm-spawn --relaunch pass the matching value. This keeps upstream's rex test contract and the fork's herdr contract unchanged.
  4. Busy-inbox verification notes: the fork's block kept and upstream's stuck-busy live-proof text dropped, because the fork's grace model removed that escalation. Documentation only; no upstream code was dropped.
  5. Review finding R2 (merge-resolution defect, fixed in this PR): the resolved held-call script treated every retained PR as a note-only link; the fork's supported-URL check is restored so GitHub and Forgejo PR links keep the structured-artifact update, with a regression test. Review finding R3 (fixed): the wedge-cap tests now acknowledge each presented wake under the fork's explicit-acknowledgement contract so each round counts real wedge escalations.

Fork-only changes upstream now supersedes

Known upstream issues carried unchanged

  • Rex absence proof (upstream feat: add auto-detected Rex session backend kunchenguid/firstmate#7633, bin/backends/rex.sh): a not-found answer from a different Rex server is taken as proof that a task's recorded endpoint is gone, so teardown or secondmate recovery could act while the worker is still alive on the original server. Affects Rex users only; the fork does not run Rex. Not fixed here: the remedy extends upstream's durable daemon record, which is beyond a catch-up merge.

Fleet-affecting changes

Commits added after the merge commit

Four by hand (tenth shard; quoted-value TOML handling; per-identity durability order; busy-inbox notes) and two by the pipeline: 6ed83546 (review fixes R2 and R3 above) and e0112f67 (documentation refresh: wording corrections in docs/, comment-only edits in bin/fm-pr-check.sh, and rewrites of docs/opencode-2-plugin-port-scope.md and docs/examples/usage-warner, which the merge itself did not change).

Pre-existing failures (not in this PR)

  • tests/fm-backend-herdr.test.sh fails on plain fork main at test_secondmate_probe_cannot_borrow_another_tasks_binding (STATE unbound in fm-secondmate-liveness-lib.sh, from fork PR 65). Every other case of the suite passes on this merge when that one call is removed.
  • tests/fm-remote-transport-lanes.test.sh fails on plain fork main: its copied remote-root/bin lacks fm-secondmate-dormant-lib.sh, which fm-ff-lib.sh now sources (fork PR 65).
  • tests/fm-lint.test.sh also fails on plain upstream af1c6817 (host-specific). Suites needing ruby (fm-test-run, fm-ci-workflow, fm-nm-test-contract) or tmux (fm-spawn-orca-worktree, fm-gate-refuse) cannot run on this host. fm-remote-job and fm-watcher-lock flaked once and passed on rerun.
  • Firstmate handles the first two separately.

Screenshots

This merge changes neither a UI nor an HTTP API, so it needs no before and after screenshots.

Risk Assessment

⚠️ Medium, for the final PR head e0112f67 (merge commit plus six follow-up commits). The merge touches dispatch, supervision, endpoint recovery and the test runner across 36 upstream PRs, but the review found no unresolved defect after the two fixes (R2, R3) and the recorded scope decision below.

  • Rex (upstream feat: add auto-detected Rex session backend kunchenguid/firstmate#7633 and follow-ups): large new backend; the fork does not run Rex. One known upstream defect (absence proof across Rex servers) is carried unchanged, listed above.
  • No CI will report on this PR (the fork's Actions has not run since 2026-10-09); validation rests on the pipeline and on about 200 non-live suites run locally. Live scenarios were not run, by design, so every behaviour check is fixture-based.
  • Two reshapes of upstream code to keep fork behaviour (held-call lookup, backend-specific absence-proof argument) are covered by their regression suites, which pass.
  • Operator action: add the tenth serial shard to any required-check list.
  • Two suites fail on plain fork main for reasons unrelated to this merge (listed above); a documentation-refresh commit from the pipeline touched upstream-authored docs and comments only.

Testing

The prior payload reports that the specified suites ran once, serially and in order, followed by successful touched-shell syntax and coverage-registration checks; no reruns were needed. Prior scratch was removed and no untracked files remained. Optional Beads-specific held-call cases were capability-skipped by the installed markdown-only tasks-axi. Retained transcripts are fixture-based evidence, not live product proof. Live validation was excluded under the reported operator-approved scope. All nine scenarios are therefore untested for live-validation purposes; fixture success does not establish a live pass.

  • Live validation: ⚠️ inconclusive - 0 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Answer a held task before cleanup replay without losing its retained PR or report ⏸️ untested no The prior payload did not establish a live result. Fixture-based entrypoint tests verified structured GitHub and Forgejo PR links, retained reports, and note fallback for Gerrit and Bitbucket, but liv…
Attempt to merge a held or replaced task and receive a refusal before merge ⏸️ untested no The prior payload did not establish a live result. The fixture suite exercised PR and local merge refusal, unreadable authority records, and task-incarnation checks only. Live validation was excluded…
Acknowledge independent stale-worker wakes so numbered escalations reach the cap without recovery-wake false positives ⏸️ untested no The prior payload did not establish a live result. The non-live watcher suite verified numbered escalation rounds, invalid-override fallback, terminal-wake suppression, horizon release, and recovery.…
Select a runtime backend and reject unknown or unavailable adapters before lifecycle continuation ⏸️ untested no The prior payload did not establish a live result. Tests exercised selection precedence, nested-runtime detection, adapter refusal, and lifecycle entrypoints with fake backend tools; no real runtime w…
Manage Rex task placement and cleanup through the adapter against a stateful server model ⏸️ untested no The prior payload did not establish a live result. The existing stateful-fake suite exercised placement, cleanup, failure responses, and event-wait behavior, but no real Rex server was launched. Live…
Generate task briefs while preserving evidence requirements, task-temp paths, and explicit Herdr-lab intent ⏸️ untested no The prior payload did not establish a live result. The suite exercised generated brief output and invalid-input refusal, but did not launch a worker or establish how a live model interprets the brief.…
Submit ship completion or Bitbucket CI-ready status and reject an unproven named head ⏸️ untested no The prior payload did not establish a live result. Disposable Git fixtures verified reachable-head acceptance and refusal of unpushed heads, unrelated moved branches, and Bitbucket CI-ready status lac…
Request repository publication without matching authority and receive a refusal ⏸️ untested no The prior payload did not establish a live result. Fixture entrypoints verified default denial, fork-target scoping, bounded consent, and inheritance-receipt checks, but no publication operation was p…
Exit or interrupt fixture work without leaving owned directories or orphan pollers behind ⏸️ untested no The prior payload did not establish a live product result. The suite observed cleanup after normal exit and SIGTERM, ownership-scoped orphan reaping, read-only tree removal, and poller termination, bu…
Evidence: Captain-hold lifecycle regression transcript

Source: Captain-hold lifecycle regression transcript

FM_TEST_BEGIN 2026-10-23T23:26:28Z tests/fm-captain-hold-lifecycle.test.sh family=pure-contract-unit expected_gate_skip=none
ok - marked hold reasons round-trip through public reads, fleet, startup, and return without changing other fields
ok - new, active, and released holds require the origin first with and without deferral
ok - historical self-inventories name a workable repair that preserves sibling entries
ok - completion and verification compare backend identities for entries and current or stored origins
ok - complete refuses the origin as its own captain-call inventory
ok - complete refuses an entry held for another origin and flags one with none recorded
ok - report-only unresolved captain call is reproduced and completion refuses before loss
ok - both body-decoding paths work without the allow_nonref default
ok - cleanup preserves every byte of a retained body's non-ASCII characters
ok - the completion gate attests captain-held inventory and transfers open status decisions
ok - answer records the captain's words, closes idempotently, and releases routed work
ok - release frees held work with the captain's words recorded and the body preserved
ok - captain holds become visible only after their hold-set timestamp is durable
ok - an interrupted answer preserves its hold age until close retry
ok - a deferred captain call leaves the live Captain's Call until its date and stays answerable
ok - an out-of-band close is recordable with the captain's word and nothing else
ok - ended visual review follows the same captain-hold completion owner
ok - resolved findings and decision-like prose do not create captain-held tasks
ok - terminal single-owner stale status decisions do not block empty inventory
ok - main-home and secondmate-home captain calls remain correctly routed
ok - a secondmate home publishes each hold occurrence and its answer on the parent channel
ok - secondmate resolutions publish before retiring durable retry triggers
ok - a bound channel's captured answers close their captain-held tasks at answer time
ok - only a bound captured source creates reconcile requests
ok - normal answers and their replays retire reconcile requests
ok - reconcile closes a moot call with evidence and keeps an active one open with a note
ok - reconcile outcomes apply durable mutations once across partial failures
ok - a channel source with no decision binding closes nothing
ok - legacy identities, metadata, bindings, and the shim keep working
ok - a board answer reaches the keyed-answer intake and wakes firstmate
ok - the chat channel feeds the same keyed-answer intake a captured review does
ok - completion and verification validate origins before constructing paths
ok - a status resolution over a still-open captain-held task is signalled, not closed
ok - a captain call with no routed work, a verified transfer, an open decision, and an answered call all stay silent
ok - cleanup leaves a captain-held work item open with its deliverable, and only an answer closes it
ok - release and scout report retention distinguish deliveries from rejected merge answers
ok - an interrupted cleanup keeps the captain call recoverable and session start retains it
ok - an answer before cleanup replay preserves the retained report
ok - an answer before cleanup replay preserves the retained GitHub pull request
ok - an answer before cleanup replay preserves the retained Forgejo pull request
ok - an answer before cleanup replay preserves the retained Gerrit pull request
ok - an answer before cleanup replay preserves the retained Bitbucket pull request
ok - an unusable pending-close record names its reason instead of a bare refusal
ok - an unsupported relocated report does not wedge the captain's answer
ok - cleanup retains captain calls in the configured backlog
ok - cleanup keeps a captain-held Gerrit task open and records its change URL
ok - merge approval releases before zero-retention cleanup records completion
ok - the PR merge entrypoint refuses a captain-held task before merging
ok - the local merge entrypoint refuses a captain-held task before merging
ok - the PR merge entrypoint separates an unreadable authority record from an absent one
ok - the local merge entrypoint separates an unreadable authority record from an absent one
ok - merge entrypoints reject unsafe identities and absent state before locking
ok - merge entrypoints refuse a replacement incarnation after waiting for cleanup
ok - merge entrypoints own task state before forced cleanup can retire it
ok - a released merge passes the guarded entrypoint and remains recently landed
ok - cleanup refuses a ship row when its captain hold cannot be read
ok - skipped on markdown-only tasks-axi: verify against a beads-migrated hold
ok - skipped on markdown-only tasks-axi: verify against a prefix-migrated hold
ok - skipped on markdown-only tasks-axi: prefer a marker-noted row over a prefix namesake
ok - skipped on markdown-only tasks-axi: complete against a beads-migrated hold
ok - skipped on markdown-only tasks-axi: verify an unresolvable beads legacy id
ok - skipped on markdown-only tasks-axi: verify a derived pre-collapse key
ok - captain-hold mutations address the beads backend without a markdown override
ok - skipped on markdown-only tasks-axi: captain-hold create under due.required without types.custom
ok - archive gates accept answered calls and refuse unanswered ones (default)
ok - archive gates accept answered calls and refuse unanswered ones (unset)
ok - archive gates accept answered calls and refuse unanswered ones (inline-comment)
ok - archive gates accept answered calls and refuse unanswered ones (single-quoted)
ok - archive gates accept answered calls and refuse unanswered ones (inherited)
ok - archive gates accept answered calls and refuse unanswered ones (project-precedence)
ok - archive gates accept answered calls and refuse unanswered ones (absolute)
ok - a live legacy row takes precedence over its earlier archived answer
ok - an unanswered archived row takes precedence over an earlier answer (held-body)
ok - an unanswered archived row takes precedence over an earlier answer (empty-body)
ok - an unanswered archived row takes precedence over an earlier answer (reused-id)
ok - an unanswered archived row takes precedence over an earlier answer (live-legacy)
ok - an unanswered archived row takes precedence over an earlier answer (live-legacy-held)
ok - an unanswered archived row takes precedence over an earlier answer (live-legacy-done)
ok - the exact archived answer takes precedence over a live legacy row for another origin
ok - an answered captain call archived by Done retention still satisfies the completion gate
ok - an answered captain call archived to a user-configured location still satisfies the completion gate
FM_TEST_END 2026-10-23T23:41:44Z tests/fm-captain-hold-lifecycle.test.sh exit=0 duration_ms=916175 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=916219
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=916175 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-captain-hold-lifecycle.test.sh duration_ms=916175
Evidence: Watcher wedge-cap regression transcript

Source: Watcher wedge-cap regression transcript

FM_TEST_BEGIN 2026-10-23T23:41:44Z tests/fm-watch-wedge-cap.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - FM_WEDGE_MAX_ESCALATIONS=0 keeps the cap inert and never writes a cap marker
ok - wedge cap fires PERMANENTLY-WEDGED at FM_WEDGE_MAX_ESCALATIONS and writes the window-scoped cap marker
ok - subsequent polls on the same hash stay silent while the window-scoped cap marker stands
ok - the window-scoped cap marker silences a hash-churning pane across all post-cap polls
ok - the cap is bound by FM_CAP_HORIZON_SECS and re-fires after the horizon elapses
ok - operator rm of the cap marker lifts the cap and the next wedge re-accumulates from 1
ok - non-integer FM_WEDGE_MAX_ESCALATIONS falls back to 0 and the cap stays inert
ok - an escalation counter the shell cannot compare is read as a fresh count of 1, never as a count the arithmetic wrapped around
ok - the rollback-failed sentinel silences the wedge path within FM_ROLLBACK_SENTINEL_TTL_SECS and still reports a gone endpoint
ok - an escalation count the state dir refuses fails safe under a configured cap and silences nothing without one
ok - a window silenced by the rollback-failure sentinel keeps the probe on its escalation cadence, not once per poll
ok - a counter reset the state dir refuses ends the cycle behind the sentinel instead of walking out with a saturated counter
ok - a genuine recovery clears the cap marker and the re-wedged pane escalates again
ok - a persistent marker-write failure across two cap cycles queues exactly one PERMANENTLY-WEDGED wake
ok - a declared wait and its lift both release the cap markers, and the re-wedged pane escalates again
ok - a capped window still reports its gone endpoint exactly once across repeated polls
ok - a new wedge episode publishes its own terminal wake behind an unconsumed earlier one
ok - a capped live endpoint is probed on its own escalation cadence, not once per poll
ok - an unwritable terminal episode marker narrows suppression instead of flooding the queue
ok - every first-sight route that ends a wedge episode releases that episode's cap and lets the next wedge escalate
ok - a non-terminal stale poll ends the episode, releases the cap, and lets the fresh wedge escalate immediately
ok - with the cap off, a leftover cap marker or rollback-failure sentinel silences nothing
ok - a cap fired on the terminal-override route still reports a gone endpoint exactly once
FM_TEST_END 2026-10-23T23:49:16Z tests/fm-watch-wedge-cap.test.sh exit=0 duration_ms=451252 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=451290
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=451252 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-watch-wedge-cap.test.sh duration_ms=451252
Evidence: Backend selection and lifecycle fixture transcript

Source: Backend selection and lifecycle fixture transcript

FM_TEST_BEGIN 2026-10-23T23:49:16Z tests/fm-backend.test.sh family=backend-dispatch expected_gate_skip=none
ok - fm_backend_name: FM_BACKEND env > config/backend > default tmux
ok - fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, $TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first
ok - fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)
ok - fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)
ok - fm_backend_detect: an inherited cmux bundle id never outranks $TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)
ok - fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain
ok - fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid
ok - fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)
ok - fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged
ok - fm_backend_name: verified Herdr and tmux stay silent while experimental cmux remains loud
ok - fm_backend_detect/fm_backend_name: paseo is never auto-detected from PASEO_* markers or its bundle id (tmux default stays silent); only FM_BACKEND or config/backend select it
ok - fm_backend_detect/fm_backend_name: TERM_PROGRAM=rex + REX_BLOCK -> rex (silent); nested tmux/herdr win; REX_BLOCK alone never selects rex; explicit config wins
ok - fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker
ok - fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly
ok - zsh: fm_backend_source recognizes known backends and rejects unknown ones
ok - zsh: fm_backend_source keeps PATH intact while loading a backend adapter
ok - bash: fm_backend_source recognizes known backends and rejects unknown ones
ok - fm_backend_source: missing adapter fails before lifecycle continuation
ok - fm_backend_source: unreadable adapter fails before lifecycle continuation
ok - fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read last key=value and default backend to tmux
ok - fm_backend_resolve_selector: session:window literal, exact task id first, legacy fm-<id> label fallback, ad hoc bare name via tmux list-windows
ok - fm_backend_of_selector: exact task ids, legacy fm-<id> labels, and matching explicit targets inherit metadata backend
ok - fm-send.sh: explicit tmux targets are verified; text types once and submits with Enter
ok - fm-peek.sh: capture-pane invocation and output are byte-identical old vs new
ok - fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal
ok - fm-teardown.sh: treehouse return remains compatible while tmux cleanup uses exact selectors
ok - fm-spawn.sh --backend bogus is refused loudly
ok - fm-spawn.sh --backend codex-app is refused
ok - fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly
ok - fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)
ok - fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker
ok - fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end
FM_TEST_END 2026-10-23T23:49:44Z tests/fm-backend.test.sh exit=0 duration_ms=28617 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=28657
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=1 duration_ms=28617 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-backend.test.sh duration_ms=28617
Evidence: Rex adapter stateful-fake regression transcript

Source: Rex adapter stateful-fake regression transcript

FM_TEST_BEGIN 2026-10-23T23:49:44Z tests/fm-backend-rex.test.sh family=rex expected_gate_skip=optional-binary
ok - fm_backend_rex_bin: REX_BIN_DIR, then PATH, then the server bundle path
ok - fm_backend_rex_version_check: accepts API revision 5 and refuses an older CLI, older server, or missing CLI
ok - fm_backend_rex_parse_target: <session-uuid>:<block-uuid> only, prefixes restored
ok - fm_backend_rex_create_task: one per-home session created once then adopted, one tab per task, scoped block label, 200x50
warning: could not move the respawned Rex secondmate tab window:00000009-0000-7000-8000-000000000009 to the first position
ok - fm_backend_rex_create_task: confirmed launcher session gets the tab focus-safe; unconfirmed claims refuse; nested tmux and other-home use the per-home session
ok - fm_backend_rex_create_task: refuses an unreadable inventory, ambiguous per-home sessions, and duplicates; a gone session returns 2
ok - fm_backend_rex_create_task_in_session: closes a bare-shell husk carrying the task label, refuses over a live one
ok - fm_backend_rex_create_task reclaim: the fallback placement closes a bare-shell husk, refuses a live one, and spawn still refuses duplicates
ok - rex capture: CR stripped and tailed; viewport derived from the last size.rows styled rows, implicit empty bottom row restored
ok - fm_backend_composer_state rex: the composer holding the cursor decides; a cursor outside any composer falls back to the cursorless read
ok - fm_backend_agent_state rex: alive/dead/ambiguous from the process view, missing on not-found, unreadable without starting a server
ok - fm_backend_busy_state rex: OSC 7501 records count only while their owner is the foreground process or an ancestor
ok - fm_backend_rex_current_path: the foreground process cwd
ok - rex input: literal text after --, no call for empty input, verified key names only, line submit
ok - fm_backend_kill rex: closes exactly its tab, treats a gone block as done, retires the emptied per-home session
ok - fm_backend_kill rex: never kills a session this home does not own, even when it is empty
ok - fm_backend_kill rex: refuses success unless the block is proven gone
ok - fm-teardown rex: retiring a secondmate closes its crew and its own tab and retires its per-home session
ok - fm_backend_rex_endpoint_absence_recheck: proof only from the recorded server; missing/dead/unproven
ok - fm_control_endpoint_absence_verdict rex: gone only on a not-found from the recorded server
ok - fm-control exit on rex: a stopped server goes through the recorded-server absence proof, and refuses when it cannot prove absence
ok - fm_backend_rex_list_live: home-scoped blocks only, nothing when unreachable
ok - fm_backend_events_capable rex: API revision 5 on both ends, read without starting a server
ok - fm_backend_wait_transition rex: a trusted blocked edge returns its record with Rex's app, kind, and message
ok - fm_backend_wait_transition rex: a blocked edge counts only while its owner is the foreground process or an ancestor
ok - fm_backend_wait_transition rex: level reconcile returns a blocked block once; working and cleanup clear the marker
ok - fm_backend_wait_transition rex: a watched block's exit or close ends the wait at once for an immediate poll
ok - fm_backend_wait_transition rex: a reader that cannot start or ends early falls back to polling
FM_TEST_END 2026-10-23T23:50:04Z tests/fm-backend-rex.test.sh exit=0 duration_ms=19582 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=19638
FM_TEST_SUMMARY_FAMILY family=rex count=1 duration_ms=19582 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-backend-rex.test.sh duration_ms=19582
Evidence: Brief-generation regression transcript

Source: Brief-generation regression transcript

FM_TEST_BEGIN 2026-10-23T23:50:04Z tests/fm-brief.test.sh family=pure-contract-unit expected_gate_skip=none
ok - fm-brief: scaffolds leave the worker role scope to the launch boundary and keep the secondmate contract
ok - fm-brief.sh: bash -n succeeds
/tmp/fm-brief.g3xXg2/heredoc-in-substitution.sh:2
ok - fm-brief.sh: no heredoc is nested inside a command substitution (Bash 3.2 parse-safe)
ok - fm-brief.sh: --help renders the complete header
ok - fm-brief.sh: no-mistakes/direct-PR/local-only briefs generate cleanly
ok - fm-brief.sh: --forge forgejo names tea in Rule 3, the direct-PR DOD, and the draft read-back, marks the delivery contract, and the closed set refuses everything else
ok - fm-brief.sh: --forge external renders the no-push rule, the review contract, and the adapter, and refuses every impossible shape
ok - fm-brief.sh: ship --mode is required and closed-set validated
ok - fm-brief.sh: the explicit ship mode wins over the registered posture
ok - fm-brief.sh: --yolo and scout/secondmate --mode are refused, never silently dropped
ok - fm-brief.sh: faster paths use configured authority without stacked review
ok - fm-brief.sh: no-mistakes DOD keeps its apostrophe prose and bans --yes outright
ok - fm-brief.sh: no-mistakes DOD detects a green PR from the drive call, not a status poll
ok - fm-brief.sh: PR-based done requires a non-draft PR; a deliberate draft declares a wait
ok - fm-brief.sh: no-mistakes ask-user findings use one event plus a verbatim snapshot
ok - fm-brief.sh: ship project-memory wording bounds edits to corrections of wrong information
ok - fm-brief.sh: --herdr-lab emits the complete hard safety contract
ok - fm-brief.sh: --herdr-lab uses its quoted Firstmate-owned helper path
ok - fm-brief.sh: ship and scout scaffolds make omitted Herdr intent fail-visible
ok - fm-brief.sh: the documented {TASK} and {FIRSTMATE_SPEC} fills cannot corrupt the Herdr safety gate
ok - fm-brief.sh: Herdr lab contract covers scouts and rejects secondmate misuse
ok - fm-brief.sh: --no-projects scaffolds a project-less charter and guards misuse
ok - fm-brief.sh: marked requests avoid generic acknowledgements and preserve material reporting
ok - fm-brief.sh: relative directory inputs ignore CDPATH, render stable absolute charter paths, or fail loudly
ok - fm-brief.sh: custom pause verb renders in every scaffold
ok - fm-brief.sh: ship and scout scaffolds declare a validation-round pause once, then hold it
ok - fm-brief.sh: ship and scout scaffolds keep scratch and --intent files in the task temp root
ok - fm-brief.sh: investigation and visual-review completions load the shared decision policy
ok - fm-brief: scout and secondmate code paths still scaffold well-formed briefs
ok - fm-brief.sh: scout Lavish hosting follows the bootstrap lavish-axi floor
ok - fm-brief: inbox listing is silent on empty/absent inbox and numerically ordered
ok - fm-brief: workers end the turn on a decision, wait in one bounded shell command, and never poll
ok - fm-brief: without config/wait-no-turns the brief and drive text stay as they were
ok - fm-brief.sh: the home brief include lands last on ship and scout, verbatim, and fails closed
ok - fm-brief.sh: --base-branch records the base, targets the PR at it, and is refused where no PR carries it
ok - fm-brief.sh: --branch-prefix omitted defaults every ship mode to fm/<task-id>
ok - fm-brief.sh: a --branch-prefix override renders identically across every generated section
ok - fm-brief.sh: an empty --branch-prefix override resolves to a bare <task-id> branch
ok - fm-brief.sh: --branch-prefix is refused on scout and secondmate scaffolds
ok - fm-brief.sh: --branch-prefix value is validated against embedded spaces and a leading dash
Switched to a new branch '$(touch${IFS}/tmp/fm-brief.g3xXg2/branch-prefix-shell-safe-marker)brief-branch-safe-g3'
ok - fm-brief.sh: ref-format-valid shell metacharacters stay literal in generated branch commands
ok - fm-brief.sh: every crewmate scaffold forbids administering the shared worktree pool
ok - fm-brief: the evidence subsection and contract appear only for a ship when config/pr-evidence enables the project
FM_TEST_END 2026-10-23T23:50:22Z tests/fm-brief.test.sh exit=0 duration_ms=17616 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=17668
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=17616 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-brief.test.sh duration_ms=17616
Evidence: Named-head and CI-ready completion-gate transcript

Source: Named-head and CI-ready completion-gate transcript

FM_TEST_BEGIN 2026-10-23T23:50:22Z tests/fm-dod-lib.test.sh family=pure-contract-unit expected_gate_skip=none
ok - scout done: is not gated
ok - unpushed ship done: is refused
ok - no-mistakes pre-validation done: is not gated
ok - named head on a remote-tracking ref is accepted
ok - a moved remote branch that lacks the named head is refused
ok - a 40-hex token in the note is not the named head
ok - a recorded merged PR satisfies the gate after prune
ok - the merged-PR short-circuit applies only to the recorded PR the done line names
ok - a forge-recorded head for the named PR is accepted without a local object
ok - a direct-PR recorded head does not cover a later unpushed commit
ok - CI-ready notes require checks green and an actual PR reference
ok - Bitbucket CI-ready done requires a durable named head
ok - no-mistakes CI-ready done: with extra text is gated
ok - keyed and spaced ship done: lines are gated
ok - local-only linked named branch is reachable from the project clone
ok - local-only detached HEAD only in the disposable copy is refused
ok - standalone local-only done: requires the named head in the project clone
ok - non-done lines are not gated
ok - fenced and indented Captain lines are not authorized intent
ok - PR-based DoD draft check uses gh-axi
ok - promotion keeps a scout's recorded base branch and refuses local-only for it
ok - forge=external composes with direct-PR only and refuses a base branch
ok - external rule one forbids origin and task-prefixed pushes and names only the backup copy
ok - external DoD renders the contract, the review grammar, and the adapter, and refuses without one
ok - external review done: accepted on a canonical URL alone without a backup; PR, non-https, query, fragment, forge, and pattern mismatches refused
ok - external review done with a backup: refused until HEAD is under refs/remotes/backup/backup/, then accepted
ok - worker role names the skill and its fallback skill file
all fm-dod-lib tests passed
FM_TEST_END 2026-10-23T23:50:28Z tests/fm-dod-lib.test.sh exit=0 duration_ms=6021 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=6065
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=6021 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-dod-lib.test.sh duration_ms=6021
Evidence: Repository publication-authority regression transcript

Source: Repository publication-authority regression transcript

FM_TEST_BEGIN 2026-10-23T23:50:28Z tests/fm-self-repo-role.test.sh family=unclassified expected_gate_skip=none
ok - fm-role.sh report: default, user, and maintainer directives resolve
ok - fm-role.sh report: malformed, unknown, and unsafe input fails closed
ok - fm-role.sh check: the default user/operator home cannot publish to firstmate upstream
ok - fm-role.sh check: a task on an ordinary project is untouched by the boundary
ok - fm-role.sh check: a matching maintainer scope authorizes the self-repository publication
ok - fm-role.sh check: scope mismatch and mere upstream remote presence never authorize
ok - fm-role.sh check: equivalent remote spellings resolve to one publication target
ok - fm-role.sh check: repository lineage, not remotes, identifies firstmate across forks
ok - fm-role.sh check: gh-resolved base selects the exact fork target, otherwise legacy precedence holds
ok - fm-role.sh check: undeterminable lineage or publication target refuses
ok - fm-role.sh check: recorded one-task consent authorizes its named repository only
ok - secondmate home: a role file without an inheritance receipt grants nothing
ok - secondmate home: inherited grants need a matching receipt; receipt-less and revoked grants are dead
ok - secondmate home: a primary role its own resolver rejects propagates as revocation
ok - fm_dod_block: upstream consent renders on publishing contracts and is refused on local-only
ok - fm-brief.sh: --upstream-consent validates, scopes to ship tasks, and renders normalized
ok - fm-brief.sh: --project applies the self-repository gate at scaffold time
ok - fm-spawn.sh: the launch gate refuses a user/operator home and passes a matching maintainer
ok - fm-spawn.sh: consent must agree between flag and brief, and is refused on local-only
ok - fm-promote.sh: the self-repository gate governs promotion identically to spawn
ok - fm-spawn.sh: --upstream-consent is refused on secondmate charters
# all fm-self-repo-role tests passed
FM_TEST_END 2026-10-23T23:50:37Z tests/fm-self-repo-role.test.sh exit=0 duration_ms=8989 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=9032
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=8989 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-self-repo-role.test.sh duration_ms=8989
Evidence: Fixture cleanup and ownership regression transcript

Source: Fixture cleanup and ownership regression transcript

FM_TEST_BEGIN 2026-10-23T23:50:37Z tests/fm-test-fixture-cleanup.test.sh family=standalone expected_gate_skip=none
ok - fm_test_tmproot cleans up its fixture root on normal exit
ok - fm_test_tmproot cleans up its fixture root on SIGTERM
ok - the cleanup registry cannot be injected through path precreation
ok - failed fixture registration rolls back the new root
ok - the orphan sweep reaps only old fixtures without a live owner
ok - the orphan sweep reaps read-only package fixtures
ok - test registries and fixture roots stay out of a git worktree TMPDIR
ok - remote worker discovery and cleanup preserve spaced paths and exact fixture scope
ok - fm_test_cleanup stops an orphaned poller that names its fixture root
FM_TEST_END 2026-10-23T23:50:41Z tests/fm-test-fixture-cleanup.test.sh exit=0 duration_ms=4066 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=4111
FM_TEST_SUMMARY_FAMILY family=standalone count=1 duration_ms=4066 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-test-fixture-cleanup.test.sh duration_ms=4066
- Outcome: ⚠️ 1 warning across 2 runs (56m59s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-captain-hold.sh:1237 - The previously approved correction is still absent. fm_backlog_pr_note_label succeeds for every URL, including supported GitHub and Forgejo PRs, so this branch bypasses the structured-artifact update at bin/fm-captain-hold.sh:1241-1242 for every --pr marker. If teardown records retention but fails returning the worktree, answering before cleanup replay closes the task with only a note instead of restoring its structured PR link. Restore the fm_backlog_row_artifact_supported condition; use the label helper only for unsupported URLs, preserve the --report path, and cover answering a supported retained PR before replay.
  • ⚠️ tests/fm-watch-wedge-cap-live.test.sh:282 - The previously approved acknowledgement correction is still absent. Draining without the generation-bound --ack-through command leaves the first wake queued; subsequent watcher cycles can report check: rearm-resurface rather than advancing the wedge ladder. The live guard counts any nonempty reason as an escalation at tests/fm-watch-wedge-cap-live.test.sh:274-276 and carries outstanding recovery state into the capped arm at :290. The same invariant is violated between invalid-override rounds at tests/fm-watch-wedge-cap.test.sh:690-705, where recovery wakes can satisfy the assertions without another wedge escalation. Acknowledge each presented wake between independent rounds and require actual numbered wedge-escalation outcomes, while preserving intentionally unacknowledged queue scenarios elsewhere.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • ⚠️ The Test agent did not finish within its invocation budget. Reported: agent run tests timed out after 30m0s: agent last produced output 8s ago (464 observed); agent reported: pi exited: exit status 143. This is a budget or provider-slowness cut, not a code failure. Re-running the same request costs another full budget, so no further attempt is made automatically. If this repository's targeted tests or evidence gathering routinely approach the default 30m0s, raise test_agent_timeout in global config. Respond with fix to spend another budget: a repair turn runs only for selected findings other than this budget cut, then validation re-runs. Or abort and retry after raising the budget.
  • 🚨 Approval is refused: the run worktree at ~/.no-mistakes/worktrees/450411b3e67c/01M5NDSRSC3QSFVJQR0FE670EK holds work no Test turn validated, and the steps after Test would commit and publish it. It holds uncommitted changes to .test-phase/ (inspect with git -C ~/.no-mistakes/worktrees/450411b3e67c/01M5NDSRSC3QSFVJQR0FE670EK status and git -C ~/.no-mistakes/worktrees/450411b3e67c/01M5NDSRSC3QSFVJQR0FE670EK diff). Respond with fix to validate it, or abort.

🔧 No changes applied.
1 warning still open:

  • ⚠️ live validation verdict: inconclusive (0 of 9 scenarios were driven live against the product); untested: Answer a held task before cleanup replay without losing its retained PR or report, Attempt to merge a held or replaced task and receive a refusal before merge, Acknowledge independent stale-worker wakes so numbered escalations reach the cap without recovery-wake false positives, Select a runtime backend and reject unknown or unavailable adapters before lifecycle continuation, Manage Rex task placement and cleanup through the adapter against a stateful server model, Generate task briefs while preserving evidence requirements, task-temp paths, and explicit Herdr-lab intent, Submit ship completion or Bitbucket CI-ready status and reject an unproven named head, Request repository publication without matching authority and receive a refusal, Exit or interrupt fixture work without leaving owned directories or orphan pollers behind
  • Live validation: ⚠️ inconclusive - 0 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Answer a held task before cleanup replay without losing its retained PR or report ⏸️ untested no The prior payload did not establish a live result. Fixture-based entrypoint tests verified structured GitHub and Forgejo PR links, retained reports, and note fallback for Gerrit and Bitbucket, but liv…
Attempt to merge a held or replaced task and receive a refusal before merge ⏸️ untested no The prior payload did not establish a live result. The fixture suite exercised PR and local merge refusal, unreadable authority records, and task-incarnation checks only. Live validation was excluded…
Acknowledge independent stale-worker wakes so numbered escalations reach the cap without recovery-wake false positives ⏸️ untested no The prior payload did not establish a live result. The non-live watcher suite verified numbered escalation rounds, invalid-override fallback, terminal-wake suppression, horizon release, and recovery.…
Select a runtime backend and reject unknown or unavailable adapters before lifecycle continuation ⏸️ untested no The prior payload did not establish a live result. Tests exercised selection precedence, nested-runtime detection, adapter refusal, and lifecycle entrypoints with fake backend tools; no real runtime w…
Manage Rex task placement and cleanup through the adapter against a stateful server model ⏸️ untested no The prior payload did not establish a live result. The existing stateful-fake suite exercised placement, cleanup, failure responses, and event-wait behavior, but no real Rex server was launched. Live…
Generate task briefs while preserving evidence requirements, task-temp paths, and explicit Herdr-lab intent ⏸️ untested no The prior payload did not establish a live result. The suite exercised generated brief output and invalid-input refusal, but did not launch a worker or establish how a live model interprets the brief.…
Submit ship completion or Bitbucket CI-ready status and reject an unproven named head ⏸️ untested no The prior payload did not establish a live result. Disposable Git fixtures verified reachable-head acceptance and refusal of unpushed heads, unrelated moved branches, and Bitbucket CI-ready status lac…
Request repository publication without matching authority and receive a refusal ⏸️ untested no The prior payload did not establish a live result. Fixture entrypoints verified default denial, fork-target scoping, bounded consent, and inheritance-receipt checks, but no publication operation was p…
Exit or interrupt fixture work without leaving owned directories or orphan pollers behind ⏸️ untested no The prior payload did not establish a live product result. The suite observed cleanup after normal exit and SIGTERM, ownership-scoped orphan reaping, read-only tree removal, and poller termination, bu…
  • Removed the untracked .test-phase/ directory and confirmed no l.* lab directories remained.
  • Ran all validation commands under env -i PATH=&#34;$PATH&#34; HOME=&#34;$HOME&#34; USER=&#34;$USER&#34; LANG=C.UTF-8 TERM=dumb NO_MISTAKES_GATE=1, leaving TMPDIR, fleet identity, task overrides, and forge credentials unset.
  • bin/fm-test-run.sh --jobs 1 tests/fm-captain-hold-lifecycle.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-watch-wedge-cap.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-backend.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-backend-rex.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-brief.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-dod-lib.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-self-repo-role.test.sh
  • bin/fm-test-run.sh --jobs 1 tests/fm-test-fixture-cleanup.test.sh
  • Ran bash -n on every changed shell file between 244d8368fb436e31962c91fc2d60cb2c7d78f16e and 6ed8354606febe990a522245fe49ed7e6fb10b86, including the live wedge guard without executing it.
  • bin/fm-test-run.sh --check-coverage
  • Confirmed git status --short and git diff --stat were empty after validation.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

LorenzoMinghini and others added 30 commits October 21, 2026 16:00
…cap (kunchenguid#7552)

* feat(fm-watch): bound consecutive wedge escalations per worker window

An unattended watcher escalation loop once burned ~359M tokens in a day: the
watcher keeps re-escalating a pane it believes is wedged, and nothing bounded
how many times it did that for one worker window.

Add an opt-in ceiling on consecutive wedge escalations for a single window.
At FM_WEDGE_MAX_ESCALATIONS the watcher publishes one terminal
PERMANENTLY-WEDGED wake and writes both cap markers, after which the wedge
path short-circuits for that window until FM_CAP_HORIZON_SECS elapses or an
operator removes both markers. A pane hash change alone does not re-engage
while the window-scoped marker stands, so a pane that re-renders on every poll
cannot rebuild the counter and re-fire every bound. A failed marker write rolls
the cap state back rather than amplifying one write failure into a wake-queue
flood, and a rollback that itself fails leaves a transient sentinel that holds
the wedge path quiet for FM_ROLLBACK_SENTINEL_TTL_SECS.

FM_WEDGE_MAX_ESCALATIONS defaults to 0, so the unconfigured path keeps today's
behavior exactly. Each setting is validated the way the rest of the file
validates its environment settings and falls back to its default on a
non-numeric value; a zero horizon or sentinel TTL falls back too rather than
degrading into a re-fire loop.

Tests drive the watcher as a real process through its public environment
surface, and a separate opt-in live guard exercises a real tmux pane holding a
wedged process in a disposable lab home built by bin/fm-lab-home.sh, covering
both halves of the contract: the cap engaging, and the cap releasing itself on
the horizon with no operator action.

* no-mistakes(review): fix(fm-watch): bound the wedge cap honestly

* test(fm-watch): make the wedge-cap live guard a falsifiable comparison

The live guard demonstrated the ceiling but could not falsify it, and it
advertised evidence that did not survive its own run.

Register both new suites in bin/fm-test-run.sh. Unregistered they fell into
the unclassified catch-all, which put the live guard outside the repo-wide
live-gate sweep at tests/fm-live-gate.test.sh - the sweep that proves every
live guard refuses together on FM_LIVE=0. The wedge-cap suite joins
watcher-wake-lock and the live guard joins live-harness-optin; the serial
lane passes no live-capability --fail-on-gate-skip token, so CI gating is
unchanged.

Run the unceiled baseline and the ceiled arm back to back on the same
workload, one differing setting, in one transcript. The baseline at the
shipped default of 0 shows the train of five consecutive wakes the cap
exists to bound; the ceiled arm at 3 shows one terminal PERMANENTLY-WEDGED
wake and then silence. State plainly in the transcript that the motivating
incident happened at the default, so it motivates the ceiling and is not
itself evidence the ceiling engaged.

Correct the post-horizon assertion. It required the terminal marker on the
very first cycle after the horizon, which the reset escalation counter makes
false: the first post-horizon wake is an ordinary escalation and the bound is
reached one cycle later. Assert the lane is audible, not which marker it
carries.

Reprint the transcript to stdout before cleanup, so the run ends in one
self-contained evidence block instead of a pointer to the temp file this
script is about to delete. Source tests/wake-helpers.sh for hash_text, and
define PANE_TEXT in this script's own scope, so no unbound-variable or
command-not-found line can appear inside the evidence a reviewer reads.

Document in docs/configuration.md what the cap covers and does not: it bounds
the wedge timer where a busy pane crosses FM_BUSY_TURN_MAX_SECS, it does not
cover the non-terminal first-sight stale surface, only a pane leaving the
stale path clears the markers, and a cap fire resets the consecutive-deferral
budget of a wedge-evidence guard because that guard is not consulted while
the cap is silencing the window.

* no-mistakes(review): fix(fm-watch): release wedge cap markers on every episode-ending reset

* no-mistakes(review): derive the live guard's window key the way the watcher does

* no-mistakes(review): fix(fm-watch): probe dead endpoints under the wedge cap, scope terminal rows per episode

* no-mistakes(review): fix(fm-watch): cap the dead-endpoint probe, real degraded fallback

* fix(fm-watch): release the wedge cap through one episode-ending choke point

The cap's release invariant - every reset that ends a wedge episode drops the
cap markers with it - was wired at two inline sites and two more were missed.
One of the misses is surface_nonterminal_stale, the hot path that ends the
episode marker on every non-terminal stale poll, so the cap's episode tracking
was broken in the common case rather than the rare one and a genuine new wedge
after a pause-and-resume stayed silent for the full horizon.

Enumeration had already failed once, so wiring the two missing sites would look
finished and be incomplete again. Make the invariant structural instead: one
choke point, wedge_episode_end, now performs every removal of the
.stale-since-<key> episode marker and clears the cap markers with it, and a new
assertion in the test fails on any direct rm of that marker outside it.

keep-counter=1 marks a route that only restarts the escalation count while the
wedge timer keeps aging. That is the same episode, not a new one, so the cap
markers are deliberately not cleared there; an existing triage test pins that
behaviour and it is unchanged.

Both proofs are falsification-tested. The completeness assertion and the
behavioural case each fail against the pre-fix watcher, and the behavioural case
additionally fails when only the surface_nonterminal_stale wiring is reverted
while the choke point still exists, so it pins the hot path specifically rather
than passing because some other route happened to release the markers.

The cap stays window-scoped with no per-hash marker, still defaults to off, and
still releases on the horizon. The episode-scoped terminal row stays one per
episode under the append lock, the documented first-sight containment stays, and
the dead-endpoint-before-silencing work is untouched.

* no-mistakes(review): fix(wm-watch): release wedge cap on every episode-end route

* no-mistakes(review): fix(fm-watch): gate cap markers, probe under sentinel, release churn

* no-mistakes(review): fix(fm-watch): release wedge cap on the away first sight

* no-mistakes(review): fail safe the wedge escalation-count write under a configured cap

* no-mistakes(test): fix wedge-cap live guard leaking its temp root

* no-mistakes(review): fix(fm-watch): keep the wedge ladder reachable under a fired cap

* no-mistakes(review): fix(fm-watch): pace the sentinel dead-record probe by threshold

* no-mistakes(test): fix(test): give the wedge-cap suite its serial-lane weight hint so its CI shard fits the 30-minute job cap

* no-mistakes(document): align wedge-cap docs with implemented release boundary

* fix(docs): correct the cap markers' lifetimes in the layout reference

The state/ listing claimed .wedge-permanent-<key> "only sits on disk until a
later cap fire overwrites it". That is false: bin/fm-watch.sh:1214-1219
_wedge_cap_markers_clear removes .wedge-permanent-<key>, .wedge-terminal-queued-<key>
and .wedge-rollback-failed-<key> outright, and it runs on every cap release,
so a release takes the marker off disk rather than leaving it for a later cap
fire to overwrite.

The same clause also left the other two markers' lifetimes and delete
semantics unstated. Each now carries its own lifetime, and the sentence
records that a cap release drops all three while only the two silencing
markers re-engage a window when deleted by hand.

Verified against bin/fm-watch.sh: FM_CAP_HORIZON_SECS bounds the cap marker
via _wedge_cap_marker_stands, FM_ROLLBACK_SENTINEL_TTL_SECS bounds the
rollback-failure sentinel, and the terminal-row marker records the episode
that queued the row and is dropped with the rest of the cap state.

* no-mistakes(document): state the watcher's cap release boundary in its header comment

* fix(fm-watch): make the wedge escalation-counter reset opt-in

The upstream maintainer reviewed the wedge-cap pull request, accepted the
live validation, and asked for the escalation-counter reset to become
opt-in: it should only kick in when the cap is configured, so a home that
has not set anything keeps today's behaviour and still gets the
deep-inspection prompt for a stuck pane.

The choke point wedge_episode_end gained a third state. The call sites
whose pre-cap shape kept the escalation counter - the first-sight stale
surfaces, where upstream dropped the wedge timer and left the count
standing - now pass keep-count, so the counter survives them exactly as
upstream's did while the cap is unconfigured. With a configured cap the
count still restarts with the released episode, which is what lets a
re-wedged window escalate from 1 again past a released marker. The other
episode-ending routes are unchanged: the pre-cap watcher reset the count
there too, so those keep resetting it in both postures.

The new parity suite proves the property rather than asserting it: it
drives one stuck-pane scenario against upstream main's own bin/ tree and
against this branch, and requires the same observable outcome for the
unconfigured case - the same ladder, the same demand-deep-inspection
prompt, no cap marker. A configured cap in the same harness fires
PERMANENTLY-WEDGED at its threshold, which is what shows the setting
really reaches the watcher, so the unconfigured cases cannot pass
vacuously.

* no-mistakes(review): fix(fm-watch): bound the wedge cap settings to a comparable range

The review step's fix agent produced this change but its own turn hit the
30-minute invocation budget, so the gate failed the step with the work
uncommitted. The work is preserved in the gate's recovery ref and in the
task's saved round patch; this commit carries it forward unchanged.

bash's test cannot compare a decimal integer outside its own range: the
comparison ERRORS rather than evaluating false, and every later comparison
of such a value therefore reads as false. A value such as
99999999999999999999 passed the digit check unchanged, so the cap never
engaged - the unbounded escalation train the feature exists to bound - and
the \"is it zero\" test errored false as well, so the keep-count sites
reset the escalation counter on a home that had configured nothing, which
is the behaviour the maintainer's request forbids. The same hole let an
unusable horizon make the cap marker expire at once and an unusable
sentinel TTL drop the sentinel on the next poll, putting the failed-write
route back on every poll.

The fix bounds the accepted range at the ONE normalization choke point,
where each value is read, rather than hardening every comparison site: a
value bash cannot compare never reaches a comparison, because an
arithmetic error is read as false at every site, which is the opposite of
what each of those tests means. Values are accepted below 10^18, which
always fits bash's 64-bit signed arithmetic, and the empty, non-digit,
zero and negative handling keeps its documented fallbacks: 0 for the cap,
86400 for the horizon, 3600 for the sentinel.

Regression cases cover a value beyond bash's integer range for each of
the three settings, and the parity suite proves the unconfigured home is
still observably identical to upstream main.

* no-mistakes(document): docs: correct wedge-cap ranges, release routes, reset contract

* no-mistakes(ci): fix the wedge-cap lint finding and pack the lane on CI measurements

Upstream CI on the rework head went red on two checks, both ours.

"Lint 1" failed on the new parity suite: two `return 1` statements behind
`fail` were unreachable, and behind them was a real bug. `fail` lives in
tests/lib.sh and exits, so inside the command substitution that reads the
baseline it ended only the SUBSHELL - the outer script continued with an
EMPTY baseline and reported a divergence with an empty trace instead of
failing. The test's own header says it is the proof and a silent skip
would be no proof at all, so the baseline now reports the specific reason
and returns non-zero, and the caller's guard aborts the whole test. A
baseline that cannot be materialised aborts the test rather than
continuing.

"Behavior portable parallel 2" failed the packing-boundary check: the
wedge-cap suites were packed on provisional LOCAL measurements that
overstated their CI cost, which left the serial lane without the headroom
the boundary check needs to place one script exactly at the budget. The
same CI run measured them for real - 374225 ms for the wedge-cap suite,
27763 ms for the parity suite, 74 ms for the live-guard skip - and those
Ubuntu measurements replace the local ones in bin/fm-test-run.sh.

* no-mistakes(document): docs: fix wedge-cap measurement attribution, document cap-off release
…lag (kunchenguid#7407)

* feat: archive retired task status history behind an opt-in flag

* fix(bin): recognize an already-retired remote home before it is recreated

Sourcing fm-wake-lib.sh through the control script's libraries creates
"$FM_HOME/state", so retiring a remote home that was already removed
fabricated an unseeded directory before validate_home ran. The
already-retired branch was unreachable, and that retirement and every
retry refused with "remote home is not a seeded secondmate home".

Check for the absent home before loading any library, accepting exactly
the retire dispatcher's arguments so an invalid option still fails with
usage, and drop the allow-absent branch that could no longer be reached.

* no-mistakes(review): Removed configurable status archive retention override

* no-mistakes(review): Remove published archives after failed status retirement
…y age (kunchenguid#7364)

* fix(bin): raise unanswered review threads again after acknowledgement

Acknowledging a maintainer signal removed the only record of the thread,
so a comment nobody answered was never raised again, including while its
task sat on a hold.

Poll now tracks each unanswered maintainer comment, review and inline
review comment on an open pull request until the contribution author
replies in it or it is resolved, and raises it again once it is a day old
(FM_CONTRIBUTIONS_REPLY_AGE) and daily after that. The wake names the
comment URL and its age.

* no-mistakes(review): Track only inline and changes-requested threads, bound notified

* no-mistakes(review): Let reviewer verdicts answer reviews, skip empty-body change requests

* no-mistakes(ci): Fixed "Behavior portable serial 6" with a test-only change. The serial 4 failure is a timing flake that this change did not cause. Serial 6 (tests/fm-pr-check-security.test.sh, "retired poll left task-a.check.sh"): this PR adds one more forge read to the contribution observer, a paginated `gh api graphql` query for reviewThreads. The test's fake `gh` answered every `api graphql` call with merge-state key=value lines. The observer rejected that output and raised the wake "contributions: observation unavailable ... review thread pages returned an unexpected shape". That wake ended the watcher cycle before the merged task-a poll retired. Invariant: the fake forge must answer every read the contribution observer makes, in that read's real shape. The other contribution reads (comments, reviews, inline comments, checks, statuses, repo) were already faked. The new reviewThreads query was the only read missing. Fix: tests/fm-pr-check-security.test.sh now answers any `api graphql` call containing reviewThreads with one empty reviewThreads page. The merge-state graphql answer is unchanged. Verified: I reproduced the failure locally before the fix. The test also passes on base fc8972c with that commit's bin/. After the fix, `bash tests/fm-pr-check-security.test.sh` exits 0 with all 47 cases ok. The only other test whose fake `gh` handles `api graphql` and runs the contribution path is tests/fm-pr-merge.test.sh. It prints no "unexpected shape" text. It fails locally with "github-zero-exit-queue-required: refusal did not name the concrete observed state", and it fails the same way on base fc8972c. So that failure predates this change, and it is not among the failing CI checks. Serial 4 (tests/fm-remote-reply.test.sh, "the NUL-bearing line was not captured"): this is a 40-second wait (800 polls of 0.05 seconds) for a procevent result in the remote-reply stream, and it timed out on CI. The test does not use fm-contributions or any file this PR changes. It passed locally twice (42 ok, exit 0), so this is CI load flakiness outside this change. I made no code change for it

* fix(bin): make unanswered review re-raise opt-in

The re-raise wake and the reviewThreads read now run only when
FM_CONTRIBUTIONS_REPLY_AGE is set to a positive number of seconds.
An unconfigured install polls exactly as before: no review-thread read,
no threads[] entry and no unanswered signal. arm copies a set value into
the generated check so the watcher honors it.

* fix(bin): refuse a zero-padded zero reply age

FM_CONTRIBUTIONS_REPLY_AGE=00 and 000 passed validation as positive ages.
They now fail like 0 does.

* no-mistakes(review): remove dead reviewThreads branch from security test fake gh

* no-mistakes(review): use portable sed -i.bak in contributions test helper

* no-mistakes(docs): name the thread URL in the unanswered-wake header text
… opt-in flag (kunchenguid#7631)

* Refuse unguarded Herdr lifecycle briefs at spawn

* no-mistakes(review): name matched Herdr command, document opt-in guard, test scaffold

* no-mistakes(lint): quote matched-command assertion so ShellCheck SC2016 passes
…an opt-in flag (kunchenguid#7630)

* fix(bin): publish a child's PR-ready line only after checks green behind an opt-in flag

bin/fm-pr-check.sh publishes the child's PR-ready line to the parent channel when it registers the PR, before the child has reported its checks green. With config/ready-after-green present, a no-mistakes registration records the PR and arms the merge poll but publishes the line only when the last done line in the child's status is 'done: PR <url> checks green' for that PR. Without the flag the behaviour is unchanged.

* no-mistakes(review): accept checks-green note with surrounding text for ready line

* no-mistakes(ci): Lint 2 was caused by this PR and is fixed. Behavior tests (Herdr) is a pre-existing flake unrelated to this PR, so no code change was made for it. Lint 2 (fixed): ShellCheck SC2034 at tests/fm-teardown.test.sh:2711 because `run` was declared with `local` but is a function defined inside the test, never a variable. Invariant: a `local` declaration names only variables the function uses. The same wrong declaration also existed in the sibling opt-in test at line 2662 (both added by this PR), so `run` was removed from both `local` lines. Verified locally: `shellcheck -x tests/fm-teardown.test.sh` returns 0 (it reproduced the SC2034 warning before the edit), `bin/fm-lint.sh tests/fm-teardown.test.sh` returns 0, and `bash tests/fm-teardown.test.sh` exits 0 with 141 passing tests, including both "fm-pr-check withholds the PR-ready line ..." and "fm-pr-check releases the PR-ready line on a checks-green report that carries surrounding text". Behavior tests (Herdr) (not caused by this PR): the single failure is tests/fm-backend-herdr-presentation-e2e.test.sh "concurrent secondmate recovery failed ... error: herdr presentation recovery could not acquire its session lock; refusing a concurrent resume". That test exercises bin/fm-spawn.sh's Herdr session-lock recovery path; this PR touches only bin/fm-pr-check.sh, docs, a SKILL.md row, and tests/fm-teardown.test.sh, and the presentation test references none of them. The identical failure appeared on the unrelated branch fm/t3-5596-successor-v3 in run 39709923982 (job 119598556006), and the flake is already tracked in open issues kunchenguid#6454 and kunchenguid#7277. Rerunning the Herdr job should be enough; fixing the lock budget belongs to those issues, not this PR
* feat(bin): add Rex as an auto-detected session backend

Add bin/backends/rex.sh, a session-provider adapter for the Rex terminal
multiplexer, and wire it through the dispatcher, spawn, control plane,
crew state, busy evidence, and away-mode supervision.

- Auto-detect Rex from TERM_PROGRAM=rex with REX_BLOCK, after tmux and
  Herdr and before cmux; explicit --backend, FM_BACKEND, and
  config/backend still win, and a nested tmux or Herdr is never read
  as Rex.
- One focus-safe tab per task: in Firstmate's own Rex session when it
  runs directly inside Rex (confirmed against the server), otherwise in
  one session per home; new blocks are resized to 200x50; every call
  names its session and block by immutable id.
- Recovery-grade agent state from Rex's process view, OSC 7501 program
  status as positive busy evidence only while its owner is in the
  foreground chain, and a viewport derived from the last size.rows rows
  of the styled capture.
- Absence proof from a structured not-found on the recorded server
  (rex_server), so fm-control exit and relaunch work on Rex and a gone
  block is reclaimed into a fresh tab in its recorded session.
- Away mode for a Rex-hosted primary runs its daemon in a dedicated Rex
  session killed by exact id.
- Docs: docs/rex-backend.md, configuration and control-plane tables,
  and a dated live verification record; tests: a stateful fake-CLI
  suite, a real-server smoke test guarded to its own fm-test- session,
  and a Rex away-launcher e2e.

* no-mistakes(review): Recover Rex reclaim after server stops; document deferred asks

* no-mistakes(document): Correct Rex documentation and consolidate stale backend guidance
…ollback (kunchenguid#7625)

* no-mistakes(review): wedge cap: guard the cap-fire counter reset with rollback

* no-mistakes(document): docs: repair shard-pack wording and wedge-cap marker shorthand

* no-mistakes(review): docs: correct away-release terminal-row bound claim

* no-mistakes(document): docs: cover third guarded cap-state write in watcher comment

* no-mistakes(ci): Reweighted the packing hint for tests/fm-supervision-host.test.sh from the local 789123 ms to 1192918 ms, the one completed successful CI sample (run 39642852135, job 119276758966), and let the LPT packer rebalance. The guard was not weakened (PORTABLE_SERIAL_MAX_WEIGHT_MS=1200000, PORTABLE_SERIAL_SHARDS=9, coverage guard and packing-boundary check untouched) and nothing was hand-placed. New packed hint sums of the 9 serial shards (ms): 1=1192918 (1 script), 2=1139923 (18), 3=1139903 (30), 4=1139922 (30), 5=1139917 (30), 6=1139886 (29), 7=1139891 (30), 8=1139880 (29), 9=1139911 (31). Total 228 scripts = the serial lane exactly; max 1192918 < 1200000, so it fits. Packing guard: bin/fm-test-run.sh --check-coverage -> FM_TEST_COVERAGE ok total=268 parallel=24 parallel_max_ms=665545 parallel_imbalance_ms=2 parallel_unhinted=0 serial=228 serial_shards=9 serial_unhinted=15 serial_max_ms=1192918 serial_budget_ms=1200000 herdr=16, exit 0. tests/fm-test-run.test.sh passes both packing contracts: 'coverage guard bounds the unmeasured share and serial packing within twenty minutes' and 'serial packing accepts the exact budget and refuses one millisecond above it'. bin/fm-doc-audience-check.sh ok (surfaces=124 local_links=851). docs/fm-test-portable-shards.md now states the same 1192918 ms for that script, citing run 39642852135 (documented duration == packed weight), and adds the required honesty note in the same paragraph style: run 39765114423 cancelled this script alone in a shard at the 30-minute job cap and upstream run 39618934309 hangs identically, so the weight is a fit to the 20-minute packing target, not a cure, and the underlying slowness is an upstream defect this branch does not own. No other file, weight, test or doc line touched. Unrelated pre-existing failure, verified by git stash at base commit 5af4072: tests/fm-session-lock-ancestry.test.sh asserts 'the pty-host was not reparented to init after the daemon ended' and fails identically on the unmodified tree (sandbox pty behaviour); the --jobs family-admission assertion in the same suite fails only because of it. Not caused by this change
…nguid#6588)

* fix(bin): restore inherited handling before main-only hand-back

* Restored downtime so an earlier engine turn could not suppress a later
  main-owned close while leaving a healthy successor behind
* Routed restoration failures through the existing Stop-hook alarm
  instead of silently discarding the hand-back
* Added host-and-hook regressions plus attended and away idle-return
  controls to separate missing monitoring from status classification

* no-mistakes(test): Assert ordered stale event in returned-driver controls

* no-mistakes(document): Document main-only hand-back downtime restoration and failure

* no-mistakes(document): Correct host header on main-only downtime republish
…unchenguid#7639)

* feat(rex): push blocked edges from Rex program status through a bounded Lua reader

Rex joins Herdr as a push-capable backend: a bounded rex do reader streams
program_status_changed, child_exited, and block_closed for the watched task
blocks, and a trusted blocked edge wakes the supervisor with Rex's blocked
kind and message in the reason. The per-pane dedupe moves into the shared
transition lib, the record gains a detail field, and gone edges end the wait
for an immediate poll. The real-Rex test isolation is shared by both smokes.

* fix(rex): anchor the composer read on Rex's cursor position

Rex's format method reports the cursor alongside the styled grid, so the
composer verdict now uses the same cursor-anchored read as tmux and falls
back to the cursorless read only where that is unknown. OpenCode 1.19 draws
a status line below its composer, which made every cursorless read of an
idle OpenCode worker on Rex unknown and refused steers, exit, and relaunch.

* test(rex): add a live composer guard for every installed harness on Rex

* docs(rex): record push-event and per-tool live evidence on Rex

* no-mistakes(document): Clarify Rex documentation and shared transition ownership

* no-mistakes(ci): Fixed flaky public-followup fixture teardown: killing only the remote worker child allowed its supervisor to restart and race directory removal. Cleanup now uses the existing whole-worker-tree stop helper. Verified the full public-followup suite, remote-job orphan-reap suite, shell syntax, and git diff checks all pass
…nd an opt-in flag (kunchenguid#7634)

* refactor(bin): share the branch-to-PR lookup through fm-pr-lib

* feat(bin): notify owned PR health changes behind an opt-in flag

* no-mistakes(review): drop status-line ownership, key checks per name, remove PR wait rule

* no-mistakes(review): rediscover the branch PR once the cached one is final

* no-mistakes(review): refuse retired re-enrollment, require marked checks, verify owned records

* no-mistakes(document): note shared branch-to-PR lookup in scripts table

* fix(bin): notify reported failures when no checks are required

* no-mistakes(document): inventory PR health flag and state, note new pr-forge member
* Isolate real Rex tests with private servers

* no-mistakes(review): Trap Rex cleanup early and prove passive-read autostart guard

* no-mistakes(document): Consolidate Rex test isolation documentation
* fix(bin): report an outdated bootstrap tool instead of a missing one

detect_local_tools printed "MISSING:" for a treehouse without --lease, a
no-mistakes below 1.46.0, a gh-axi below its floor, and an incompatible
tasks-axi or quota-axi, so a supervisor read an installed tool as absent
and took the first-install remediation again.

Report those as "OUTDATED:" with the installed version, the requirement
and the tool's own upgrade command: treehouse update, no-mistakes update,
gh-axi update and quota-axi update. tasks-axi ships no self-updater, so it
keeps its install command. "MISSING:" stays for absent tools only, and
lavish-axi keeps PRESENTATION_UNAVAILABLE with its text fallback.

Update the bootstrap-diagnostics handler, the AGENTS.md section 13 trigger
list and the operator diagnostics in docs/configuration.md, and cover
absent, below-floor and at-floor for every floored tool in
tests/fm-bootstrap.test.sh.

* no-mistakes(document): Documented distinct bootstrap tool diagnostics

* fix(bin): reuse the tasks-axi check's own probe results

Rendering the tasks-axi OUTDATED requirement text ran the two `--help`
capability probes again after the compatibility check had already run
them, including when session start handed bootstrap an incompatible
verdict. The check now records the reason behind its verdict - the
version floor, or the capability probes the build fails - and reuses it,
both within the process and across the existing one-hop handoff, so
naming a missing capability costs no second probe pass. A build below the
floor is answered by its version alone and never pays for capability
probes it cannot use.

The bootstrap tests that simulate a tool as absent removed it from the
fake bin directory while bootstrap kept searching the fallback base PATH,
so a real host install could satisfy the lookup instead of the expected
MISSING line. Those cases now hide the tool from the base path as well,
and a new case pins the contrast between a bare and a curated fallback.

Verified: the touched bootstrap suite passes, and its probe-count and
capability assertions fail against the pre-fix scripts. Run with a
fallback base PATH carrying real installs of the absent tools, the
pre-fix suite fails on "absent treehouse reports a missing tool" while
the fixed suite passes every case.

* no-mistakes(document): Documented missing versus outdated tool diagnostics

* no-mistakes(document): Refresh outdated-tool documentation

* no-mistakes(review): Reuse shared version parser for outdated diagnostics

* no-mistakes(document): Correct outdated-tool diagnostics documentation

* no-mistakes(document): Align toolchain upgrade documentation

* fix(skills): keep main's bootstrap-diagnostics description and only add OUTDATED
…e verbatim match (kunchenguid#7530)

* Clarify ahoy legacy startup exclusion

* Fix quoted payload code span
…unchenguid#7637)

* fix(timeout): bypass shell builtins when invoking external runners

* no-mistakes(review): Remove redundant slashless runner resolution from external timeout
…urable (kunchenguid#6094)

* fix: count answered captain holds in the Done archive at the completion gate

Done retention moves closed markdown rows into the configured Done archive,
which tasks-axi show never reads, so verify, complete, and scout teardown
refused an answered captain hold once retention archived it. The completion
gate now reads the newest archived block for an attested entry, under its
exact or legacy identity, and accepts it only through a recorded resolution.
Unanswered archived closes and holds found nowhere still refuse.

* no-mistakes(document): document Done-archive durability in captain-hold verification record

* no-mistakes(ci): Fixed the selected Greptile findings in bin/fm-captain-hold.sh (all in the Done-archive read path added by this PR). code_change_needed=true. ci-3 (Archived reads can hang): task_show_archived() now bounds the replayed archive `tasks-axi show` with the same read bound the live backlog read uses (FM_BACKLOG_ROW_TIMEOUT_SECS via fm_run_timed, sanitising a non-positive/padded value to 10s, mirroring fm_backlog_row_show). A bound hit is treated as 124, not absence: task_show_archived returns 124 and prints a loud reason; archived_entry() propagates 124; verify_entry_durable() exits 124 on the archived-read bound (alongside its existing resolve 124 handling); verify_hold_durable() exits 124 rather than reading the row as absent. So verify can no longer hang and complete can no longer stay blocked while holding the origin metadata lock. ci-4 (User-level archive setting ignored): added captain_markdown_archive_setting(), which resolves the [markdown] archive key with tasks-axi's own source precedence — the backlog root's .tasks.toml first, then $HOME/.tasks-axi/config.toml — before the existing default of done-archive.md beside the backlog; a relative value still resolves against the backlog root, which I verified empirically matches tasks-axi (root config wins over home config; a relative home-config value resolves against the backlog root; default stays data/done-archive.md). tasks-axi exposes no command for the archive path, so re-parsing with matched precedence is the available approach. Added test_archived_answer_in_a_user_configured_archive_satisfies_the_gate, which sets the archive only in a fixture $HOME/.tasks-axi/config.toml and drives the whole hold→answer→retention→verify/complete/teardown flow with that HOME, asserting the answered call archived to the relocated location still satisfies the gate. ci-5 (repeated archive scans, P2): left unchanged per the instruction's opt-out. A clean reuse of the row archived_entry already found is not small: archived_entry runs inside a command substitution, so its TASK_SHOW_OUTPUT dies with the subshell and cannot be handed to verify_hold_durable without restructuring both to pass the loaded row through the current shell. The growth is bounded (O(archive size) per attested entry) and P2, so hardening it would add more machinery than the fix warrants. Unanswered archived closes and holds found nowhere still refuse (existing test coverage retained). Verification: `bash -n` clean; pinned ShellCheck 0.11.0 (`shellcheck -x bin/fm-captain-hold.sh`) reports 0 findings; tasks-axi archive-precedence behaviour confirmed by direct experiment. The full fm-captain-hold-lifecycle.test.sh suite (which exercises both the pre-existing archived-gate test and the new user-config test) is slow due to beads spawns and was still running at report time; recommend confirming it green before merge. ci-1/ci-2 need no code change (action_required fork workflow runs awaiting maintainer approval), per the recorded user decision

* no-mistakes(document): Document Done-archive reads under backlog row timeout

* docs: say only the archive row replay is bounded, not the archive scan

* no-mistakes(document): Move Done-archive rule to the complete section
…kunchenguid#7627)

* fix(bin): optionally read a PR with no CI checks as unverified, not checks green

nm_ci_checks_state maps the ci log marker "no CI checks reported - still
monitoring" to green, the same verdict as "all CI checks passed". A pull
request that no check ever ran against therefore surfaced as "checks green: PR
ready for review" (kunchenguid#3216).

Add a no-checks verdict for that marker, returned only in a home that creates
config/ci-no-checks-not-green. The run-step consumer reports working with a
detail naming the real condition, and the status-log override no longer
promotes a crew's own "checks green" report while that marker is the latest.
Without the flag every reading is unchanged.

Co-authored-by: Harlan Do <hieudtr8@gmail.com>

* no-mistakes(document): document ended-monitor reading under the zero-checks flag

* fix(bin): read a PR with no CI checks as unverified on every home

The ci log marker "no CI checks reported - still monitoring" now always
yields the no-checks verdict, so a pull request that no check ran against is
reported as working instead of checks green (kunchenguid#3216). The opt-in flag, its
docs section, and its skill entry are removed, the default-path test now
expects working, and the usage header and architecture note describe the
no-checks reading.

---------

Co-authored-by: Harlan Do <hieudtr8@gmail.com>
… test root-safe (kunchenguid#7650)

* no-mistakes(review): correct the counter-reset log line, the sentinel comment, and the root proof

Three findings the review step recorded but never applied, all in this branch's own change.

The clean-rollback branch of the counter-reset guard logged "cap state rolled back", copied
from the sibling marker-write branch. That is wrong here: the cap marker was already written
before this block and _wedge_cap_rollback only truncates the counter, re-arms the since-file
and clears write tracking - it never clears the markers. The window therefore stays silenced
for FM_CAP_HORIZON_SECS, so the one operator-facing line must say that rather than claim the
cap state was rolled back. The rollback behaviour is unchanged and no marker-clear call is
added: the marker is deliberately kept so the window stays quiet.

The sentinel short-circuit comment opened "from either route that has to defend cap state"
and then enumerated three routes. Wording only.

The new regression case injected its fault with chmod 444, which does not restrict root, so
under root the guard would never fire and the case would fail on its own exit-code
assertion. It now skips as root the way this suite's other mode-based faults do. The
assertions are untouched: as the ordinary user the case still proves the guard.

* no-mistakes(review): name both ways a capped window leaves the silence in the reset log line

The tier-0 message for a refused post-fire escalation-counter reset says the cap marker
stands, so the window stays silent until its horizon elapses. That names one release route
where there are two: wedge_episode_end drops the marker through _wedge_cap_markers_clear, so
any episode-ending route ends the silence early, and docs/configuration.md records deleting
the marker as the operator's way to re-engage a capped window at once. This tier writes no
sentinel, so the marker is the only thing left to remove.

The message now names the horizon, an episode-ending route, and the operator remedy, in one
line in the same shape and position as its sibling tier-0 messages. The rollback behaviour,
the marker handling and the tier are unchanged.

* no-mistakes(document): correct root-skip comment's mode-bit precedent in wedge-cap test
* fix(bin): resurface wakes appended while a handling successor is parked

The supervision host parks on the handling successor it starts for a wake
its engine handled, and a handling successor never resurfaced recovery, so
a row another script appended during the park (a captain inbox note from
the Basecamp integration) sat queued until an unrelated close - four hours
in the observed case.

A handling successor now records the newest queued sequence at its start
and closes once with the recovery wake while a newer row is still queued;
a newer row already consumed only advances that mark, so the predecessor's
delivered episode is still never re-announced.

Follow-ups, not built here:
- A row appended in the short window between a handled close and its
  successor's start (about a second) is below the successor's mark; if the
  engine handles that close without claiming the row, the row waits for the
  next close. Closing that window needs the host to treat rows left queued
  after a handled attended turn as main's, which is a separate host change.
- After a machine reboot the primary session does not restart on its own,
  so queued wakes wait until it is resumed by hand. Either auto-resume the
  primary session after boot, or alert the owner when the primary session
  is down while wakes are queued.

* no-mistakes(review): Close handling-successor startup wake race

* no-mistakes(test): Stabilize parked-host inbox wake regression timeout

* no-mistakes(document): Clarify handling-successor wake continuity guarantees

* no-mistakes(ci): Stabilized the flaky remote secondmate lifecycle test by extending the auto-relaunch watcher deadline from ~30 to ~60 seconds. The CI runner exceeded the old deadline by about 3 seconds; the changed production branch was not reachable in this fixture. `bash -n` and `git diff --check` pass. The full test was attempted but exceeded the 600-second local execution limit before reaching this case

* no-mistakes(ci): Fixed flaky concurrent Herdr recovery testing by retrying only documented session-lock contention after both competitors finish. Cleanup now handles fixture-owned read-only hooks. Targeted E2E passed; bash syntax and diff checks passed. Shellcheck reported only existing SC1091 source-following notices

* no-mistakes(test): Preserve successor wake boundaries across handoffs

* no-mistakes(test): Revert successor boundaries and drain teardown fixture wake

* no-mistakes(test): Notify main when downtime handback publication fails

* no-mistakes(test): Revert downtime handback notification changes

* no-mistakes(document): Correct stale supervision wake routing documentation

* fix(bin): stop the supervision host deadlocking and re-announcing handed-back closes

Two defects in this branch's handling-successor changes surfaced as the
"Behavior portable serial 2" shard hanging at its 30-minute limit.

A TERM that lands while the host settles a handled turn's successor handoff
left the host holding the wake-queue and recovery-marker locks; its exit
cleanup then waited forever on the queue lock it still owned. The cleanup now
releases those locks if this process owns them before releasing its grant.

When an accepted close turns main-only at its turn, the host hands it back and
leaves the successor running, but republished the episode as pending
downtime, so that successor re-announced the same close and exited. The
handback now republishes the episode as already announced; a later durable
append still opens a pending episode the successor recovers.

* no-mistakes(test): Park on successor after handling settlement failure

* no-mistakes(test): Use tab-delimited handled-settlement failure logs

---------

Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com>
* fix(bin): keep accented letters in pending-reply request summaries

fm_pending_reply_summarize deleted every byte outside printable ASCII, so a Portuguese summary lost its accents: "não faça a correção" was stored as "no faa a correo". It now removes control characters only. The 120-character cap counts UTF-8 characters from lead bytes, so it never splits a letter under any caller locale, including the C locale.

* no-mistakes(document): Documentar preservação de acentos nos resumos

---------

Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com>
…id#7149)

* Add explicit OpenRouter Jev dispatch resolution

* no-mistakes(document): Clarify dispatch documentation and remove stale duplicates

* chore: exclude local Stele binding from PR

* no-mistakes(document): Correct dispatch help and never-send documentation

* ci: retrigger CI after a timed-out test shard

* no-mistakes(document): Correct dispatch architecture documentation ownership
…imits (kunchenguid#6983)

Opt-in: fm-watch.sh publishes state/lane-capacity.json via bin/fm-capacity.sh --publish only while config/lane-capacity exists; adds beat pressure and keyed limits to fm-capacity.sh. Unconfigured fm-capacity output unchanged.
…#5359)

* fix(bin): transport contribution-input JSON through slurpfile, not argjson

bin/fm-fleet-snapshot.sh --contribution-input passed the full backlog and
task-record JSON to jq through --argjson. A single --argjson argument is
capped by the kernel's MAX_ARG_STRLEN (131072 bytes on Linux), so a large
enough backlog made the kernel refuse to exec jq while the script still
exited 0 with a broken result.

Transport both documents through temporary files and --slurpfile, matching
the pattern the default snapshot path already uses, and propagate a
non-zero exit when that jq invocation fails.

Fixes kunchenguid#5349

* test: narrow contribution-input regression to final assembly failure

The ARG_MAX transport is already fixed on main by kunchenguid#6830, so keep only the regression proving a failing final jq assembly exits non-zero.
* Fix Rex secondmate session lifecycle

* no-mistakes(review): Retire Rex secondmate sessions under the owning home identity

* no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches

* no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation
…gins, and exit (kunchenguid#7336)

* feat(opencode): add OpenCode 2 support with version-gated launch and plugins

OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.*
plugins now export a default definition with an id and a setup function
alongside their legacy OpenCode 1 server export, backed by a new shared
lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check,
pretool-check, and sessionstart-nudge install their behavior from setup(ctx)
on OpenCode 2 and keep the server entrypoint for OpenCode 1.

fm-spawn probes the resolved opencode executable's --version once and branches
the launch, model flag, reasoning config, exit key, and busy-state plugin off
the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model,
the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the
session.execution.* busy plugin); OpenCode 1.x and any undetected or
unparseable version fall back to the 1.x shape (--model, 1.x config, /exit)
everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1
keeps the pre-port session.status/session.idle plugin so its busy-state wiring
is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter
and supervision docs, OpenCode 2 live fixtures, and regression tests for the
unknown->1.x launch path and the version-gated busy-plugin shape are included.

* no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd

* feat(opencode): launch detected OpenCode 2 workers with --standalone

OpenCode 2's default launch attaches to the shared background service, which
hosts plugins with the service's environment and ignores the pane's
OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions,
FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end

OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory
and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never
found its root or delivered a prompt. Interrupted and failed turns now end a
turn like a succeeded one, while a server shutdown does not.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e

A private opencode serve plus a loopback provider proves the five primary
plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection
guards, turn-end guard, watcher arming across succeeded, interrupted, and
failed turns. The fixture declares the complete model capabilities object,
which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs
it across several OpenCode 2 executables. The credentialed half now picks its
run flags and session tables by detected major, so it keeps working on 1.x.

Fixture ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent

OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it.
After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts.

* docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes

Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate.

* no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit

* no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence

* no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation

* no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs

* test(opencode): verify OpenCode 1.x live with the dual-export plugins

Pin the live e2e TUI panes to the resolved opencode executable, and record
OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the
turn-end guard, watcher continuity, and 1.x worker launch and exit.

* no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs

* no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes

---------

Co-authored-by: Niranjan <nniranc@gmail.com>
Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t the most recent group member (kunchenguid#7662)

* fix(bin): resolve the tmux container from the caller's own session, not the most recent group member

* no-mistakes(test): Fall back when the $TMUX session id is stale

* no-mistakes(document): Document tmux container choice under grouped sessions
…er (kunchenguid#7647)

* fix(bin): read a fully dim Cursor placeholder as an empty composer

cursor-agent 2026.10.19 draws the composer glyph and its idle placeholder
as one SGR 2 run with no reverse-video cell, so ghost stripping empties the
row and the empty-content guard returned unknown. Relaunch and exit then
refused on every idle Cursor pane on tmux.

When a styled bare row strips to empty and the plain row is an agent glyph
followed by an anchored idle placeholder, read it empty. Bright typed text,
other dim text, and unstyled rows keep their existing verdicts.

Fixes kunchenguid#7413

* docs(verification): record the fully dim Cursor placeholder verdict and refresh command
… composer as empty (kunchenguid#6163)

* fix(bin): recognize a named Claude session's titled composer rule on cursorless backends

`claude -n <name>` right-aligns the session name in the rule above the
composer. Cursorless backends (Herdr) accepted only solid rules, so the
scan saw the lower rule alone, the idle composer read `unknown`, and
away-mode delivery to a named Claude primary deferred until return.

A rule carrying one right-aligned title now opens a separator pair. It
never closes one, the pair it opens counts only over an agent-glyph row,
and it is never a valid Pi pair, so Pi's verdict is unchanged.

The away-mode composer deferral now names which refusal it was:
unsubmitted text, or no composer shape recognized.

* no-mistakes(review): Reword composer deferral reason to cover busy harness

* no-mistakes(ci): ci-3 (Greptile, user chose to fix): The rule that must always hold is that Claude's titled opening rule is accepted for any non-empty session name that doesn't start with a space and doesn't contain the rule glyph `─`, including a multiword name. The only place that check lives is the title `case` in `_fm_composer_titled_rule_row` (bin/fm-composer-lib.sh). It now reads `case "$title" in ''|' '*|*─*) return 1 ;; esac`, as the user asked, and the comment above the function mentions multiword titles. I added regression fixtures to `test_matrix_claude_named_session_titled_rule` in tests/fm-composer-lib.test.sh, next to the existing single-word `Andrew` cases. They use the title `Andrew Chen` and check that an idle composer on herdr reads `empty` and a composer with typed text reads `pending`. Something you should know: the old pattern already accepted a title with a space in the middle. It only rejected a title ending in a space. So the new multiword fixtures also pass against the old code, and the bot's failure scenario did not actually happen for "Andrew Chen". What the fix really changes is that it drops the trailing-space rejection, as instructed. Checks run: tests/fm-composer-lib.test.sh and tests/fm-composer-ghost.test.sh both pass (exit 0, no failures), and `shellcheck -x` is clean on both changed files

* no-mistakes(review): Match unnamed-ghost fixture rule width to the partner rule
…process groups (kunchenguid#7666)

* fix(tests): reap Pi test process groups and stop arms whose ground is removed

Orphaned stub-arm pollers from tests/fm-pi-watch-extension.test.sh outlived
a failed case: they waited for release/stop files the failed driver never
wrote, teardown only removed the temp root, and the loops had no other exit.

Run each node driver in its own recorded process group and reap those groups
in teardown before the root is removed; end every blocking stub loop once its
own script is gone. bin/fm-watch-arm.sh now exits with a typed ground-removed
line within one poll when its script, state directory, or home is deleted,
instead of polling through the successor window.

* no-mistakes(document): Document arm exit when its ground is removed
…alidating (kunchenguid#5947)

* fix(bin): defer wedge escalation while an attributed validation run is running or fixing

While a no-mistakes run executes a round, the worker is correctly idle behind
its own blocking drive call, yet the watcher's wedge timer escalated a possible
wedge every FM_STALE_ESCALATE_SECS. The timer now consults the attributed run
step before escalating: a run the current-state reader reports as
validating (running) or validating (fixing) for a named run defers onto the
FM_PAUSE_RESURFACE_SECS recheck cadence, aged from a marker bound to that run
id. Parked, terminal, ci-monitoring, coarse, runless, absent and unreadable
runs keep the unchanged escalation ladder.

* test: count only the validating-run read in the unarmed parked-gate probe

* no-mistakes(document): add .validating-* to watcher-internals marker inventory

* no-mistakes(document): Update fm-watch.sh comments for validating-deferral chain

* no-mistakes(document): Document validating-run deferral in guard and handoff docs

* feat(watch): gate the validating-run wedge deferral behind config/wedge-defer-validating

The deferral changed the default for every home, unlike the comparable
config/wedge-defer-parked-gate and config/wedge-defer-worker-wait waits.
Absent the new presence flag the wedge timer spends no current-state read
for it and keeps the unchanged ladder.
cr101 and others added 13 commits October 23, 2026 03:35
…unchenguid#6300)

* fix(session-lock): identify a harness session on Windows/Git Bash

Every session start on Cygwin (Git for Windows) refused the fleet lock and
dropped to read-only with "cannot locate harness process in ancestry", so
spawning, steering, merging, the wake-queue drain, and supervision repair were
skipped on every start. Two independent causes, both on the identity path.

Cygwin's ps has no -o option at all and fails the whole invocation with
"unknown option -- o", so the ancestry walk aborted on its first hop. The walk
now reads comm, args, and ppid through accessors that fall back to Cygwin's
fixed ps columns, leaving the procps/BSD path unchanged.

That alone does not resolve the session: the parent link from a shell the
harness spawns does not cross the Cygwin boundary, and Cygwin reports that
shell's PPID as 1, so no walk can reach a harness that is a native Windows
process. Identity is instead taken from the session pid the harness publishes
and confirmed against the Windows process table before it is used - the pid
must still be live and its executable must independently identify a verified
harness - so an absent, stale, or non-harness value is discarded rather than
bound.

Walking the real Windows parent chain was implemented and then removed as
unsafe. MSYS emulates exec by spawning a fresh Windows process and exiting the
old one, so intermediate shells vanish and a child's recorded parent is
routinely a pid that no longer exists; Windows never reparents an orphan, so
that dangling id stays and can be reissued to an unrelated process. Following
it can bind a home's lock to the wrong process, which is the failure this file
exists to prevent. A harness that publishes nothing stays unresolved, which
leaves the session read-only exactly as before.

Windows pids are tagged rather than stored bare. They are a different namespace:
kill -0 reports a live Windows process as dead, and the number can collide with
an unrelated live Cygwin pid. The tag makes the value non-numeric, so a consumer
that treats it as a local pid - including a future kill - refuses it instead of
acting on the wrong process.

fm-sessionstart-nudge.sh carried a private second copy of the ownership walk and
so stayed wrong after the owner was fixed, nudging a session that already held
the lock. It now asks the owning function.

Verified on Windows 11 (Git Bash, Cygwin ps 3.4.10): the lock is acquired,
reports its holder, is idempotent, and refuses a bogus, dead, or non-harness
published pid. Regressions cover both platform departures behind a fake process
table, so they run on Linux and macOS CI too. The pre-existing e2e failure in
this suite on Windows is unchanged from main; it cannot exec its symlinked
fixture.

Refs kunchenguid#3396

Claude-Session: https://claude.ai/code/session_01F9T29YDqYSkQeDTC2Nfi7h
(cherry picked from commit 8b281b7)

* fix(session-lock): keep the nudge's own ancestry question intact

Delegating the nudge to fm_session_lock_owned_by_self changed the question it
asks. The nudge asks whether a process in this ancestry took the lock; the
ownership predicate additionally requires a verified harness in that ancestry,
so a session whose lock was written by a plain shell started being nudged to
run session start again. tests/fm-sessionstart-nudge.test.sh pins the looser
contract deliberately.

The walk stays local, now reading ppid through the portable accessor so it also
survives a ps with no -o option, and defers to the ownership predicate only for
a Windows-tagged holder, which is not in this process table at all.

(cherry picked from commit 0baf64f)

* fix(session-lock): accept a tagged identity in every gate that reads the lock

The Windows identity added in this branch introduced a second shape into
state/.lock. Six gates read that field, and each one answered "is this value
usable" with its own inline numeric test, so every one of them read a valid
Windows holder as malformed.

The visible cost was concentrated in startup completion: the record was never
written, and the clear/compact check that consumes it could never match, so
every clear or compact on Windows repeated the full startup sequence. The
deferred network sweeps reported ownership as changed when it had not, and the
Stop auto-arm treated a dead Windows session as an unreadable lock rather than
a recoverable one. fm-lease.sh was the outlier: rather than refusing a value it
could not use, `tr -cd '0-9'` reduced the tag to its digits and produced a
number naming an unrelated process in the local table. It happened to fail
closed downstream, but deriving a wrong-namespace pid is precisely what the tag
exists to prevent, so it now takes the value whole and requires a local pid.

fm_session_pid_valid is now the single owner of that question and every gate
delegates to it, so a third identity shape cannot split them again.

Verified against the shipped bytes of each gate with a tagged lock: startup
completion now records and is recognized (main's gate reruns the full startup
on the same input), both network-ownership gates authorize their sweeps, and a
tagged lock yields no lease holder pid instead of 7204.

(cherry picked from commit 9256182)

* fix(session-lock): gate the tagged identity on Windows and read it in every lock reader

Accept win:<digits> only where fm_win_boundary_applies, with an all-digit
remainder, so a Linux/macOS reader of a win:N lock stays inert as before.
The sessionstart nudge routes its tagged case through fm_win_untag_pid.
fm_session_lock_foreign_owner_live and fm_session_lock_inspect now validate
through fm_session_pid_valid and resolve a tagged holder through
fm_harness_pid_alive, so a live win:N holder reads as a live foreign owner
and as held; unverifiable tagged holders stay unknown.

* no-mistakes(document): Document Windows tagged session-lock identity in nudge and scripts docs

* fix(session-lock): keep task leases and stale detection working for a tagged holder

A Windows session can now hold the session lock as win:<pid>, but the task
lease still accepted only a local pid: the claim recorded the short-lived
fm-lease.sh process instead, and fm_lease_live rejected the tagged lock, so
the lease was never live and the other supervision actor could clear it and
act on the same task. The lease now records a live tagged lock holder whole
and treats it as live while it is still the lock holder and a verified
harness in the Windows process table. The session-lock library is loaded
only for a win:-shaped value, so POSIX lease verdicts are unchanged.

fm_session_lock_inspect left an exited tagged holder unknown, because
fm_win_command could not tell an absent pid from an unreadable table. It now
returns 1 only when a readable table proves the pid absent and 2 when the
table cannot be read, so inspection matches the local-pid contract: absent
is stale, a live non-harness is unknown with no live harness, and an
unreadable table stays unknown.

* no-mistakes(review): Restrict caller-supplied lease holder pid to local numeric

* no-mistakes(review): Pass tagged session-lock holder whole to task lease

* no-mistakes(review): Make tagged lease-holder regression tests fail without their fixes

* fix(session-lock): take a Windows CLAUDE_PID only with its session id

On Windows the session identity comes from CLAUDE_PID, which every process
started from a Claude pane inherits, so an unrelated process carrying a copy
could take the lock as that session and both would own it. The bridge now
accepts CLAUDE_PID only together with CLAUDE_CODE_SESSION_ID. The trusted
session-id gate matches the tagged win:<pid> form, so that id is recorded
beside the lock, and the anchor stays tagged. fm-lock.sh and the ownership
readers refuse the same tagged pid under a different recorded id as another
live session's. A non-Claude primary hand-started from a Claude pane inherits
both values and still resolves as that session; that remains a known
limitation. POSIX behavior is unchanged: every new branch applies only to a
tagged pid.

* fix(session-lock): let a Windows /clear re-key its own lock

/clear keeps the Claude process, so CLAUDE_PID stays while the session id
changes. Refusing the same tagged pid under a different recorded id locked a
session out of its own lock after /clear, which POSIX handles by re-keying
the sidecar. Drop that refusal: a Windows CLAUDE_PID is still taken only
together with its CLAUDE_CODE_SESSION_ID, and that id is still recorded
beside the lock and re-keyed on /clear as on POSIX.

* no-mistakes(review): Give tagged host test a session id; clarify Windows lock error

* no-mistakes(document): Point the trust-gate comment to the Windows limitation

---------

Co-authored-by: Lloyd <lloydkakunguwo@gmail.com>
…d#7421)

* feat(bin): add opt-in local Claude usage threshold warner

A usage window is exhausted without warning: work is cut off mid-run
rather than stopping deliberately, and nothing says a window is close to
its cap until it is already gone.

bin/fm-usage-warner.sh is a threshold warner, not a usage viewer.
quota-axi is already the one Claude-quota reader this repo depends on
(bin/fm-quota-axi-lib.sh), so the warner runs that existing read rather
than adding a second reader that could drift from it: one bounded
`quota-axi --provider claude --json --full --no-credential-refresh`, the
snapshot validated with fm_quota_json_valid, and the provider row picked
with the same FM_QUOTA_ROW_JQ join the dispatch consumers use, under
schema 5 and schema 6 alike.

Thresholds are `<window-id>:<percent>` directives in config/usage-warner,
naming quota-axi's own window ids (five_hour, seven_day, model:<name>)
rather than a second vocabulary, and compared against each window's own
percentUsed. Naming the window is the point: the session and weekly
windows exhaust at very different times, so a figure that does not say
which window is close misleads.

One read with two callers and no new schedule: `check` is what a human
runs on demand and what the watcher runs on its ordinary check-shim
cadence once `arm` registers state/usage-warner.check.sh through
bin/fm-check-register.sh. There is no daemon.

Warnings post through macOS Notification Center with the same osascript
call bin/fm-supervise-daemon.sh's wedge alarm makes, without sourcing or
coupling to that daemon's away-mode-specific config and rate limiting.
Delivery is edge-triggered against state/.usage-warner: a crossing warns
once, stays quiet while it holds, and re-arms when the window next reads
back below its threshold, the same standing-report contract
fm-mail-check.sh and fm-tool-update-check.sh already use.

bin/fm-procevent-quota.sh already reads the same snapshot, so docs name
the boundary rather than leaving two similar surfaces unexplained. That
source fires at most once, wakes firstmate with a durable check event,
and compares a provider's aggregate effectivePercentRemaining. This
warner is the standing, human-facing half: per-window, delivered to the
person through Notification Center, and self-re-arming. Neither replaces
the other and arming both is reasonable.

Opt-in is load-bearing and enforced in code. With no valid directive,
`check` returns before any read and `arm` refuses, so an unconfigured
home gains no behaviour change, no new failure mode, and no new hard
dependency at dispatch. Notification Center is macOS-only, so `arm`
refuses on other platforms instead of registering a check that could
never speak.

The warner only warns. It does not stop or pace dispatch, and its
coverage is exactly what quota-axi can see locally, so a plan or
organisation spend cap quota-axi does not report is not covered; the doc
says so rather than implying coverage it does not have.

The new test script carries no measured CI duration hint yet, so the
portable-serial lane balances it on the conservative default until the
hints are next refreshed from a green run, per docs/fm-test-portable-shards.md.

* no-mistakes(review): Key usage-warner de-dupe on window and threshold; fix docs

* no-mistakes(review): Keep absent-window crossings recorded; stub osascript in tests

* no-mistakes(document): Document absent-window de-dupe carry-forward in usage-warner header

* fix: correct Rex secondmate grouping and lifecycle (kunchenguid#7656)

* Fix Rex secondmate session lifecycle

* no-mistakes(review): Retire Rex secondmate sessions under the owning home identity

* no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches

* no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation

* feat(opencode): add OpenCode 2 support with version-gated launch, plugins, and exit (kunchenguid#7336)

* feat(opencode): add OpenCode 2 support with version-gated launch and plugins

OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.*
plugins now export a default definition with an id and a setup function
alongside their legacy OpenCode 1 server export, backed by a new shared
lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check,
pretool-check, and sessionstart-nudge install their behavior from setup(ctx)
on OpenCode 2 and keep the server entrypoint for OpenCode 1.

fm-spawn probes the resolved opencode executable's --version once and branches
the launch, model flag, reasoning config, exit key, and busy-state plugin off
the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model,
the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the
session.execution.* busy plugin); OpenCode 1.x and any undetected or
unparseable version fall back to the 1.x shape (--model, 1.x config, /exit)
everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1
keeps the pre-port session.status/session.idle plugin so its busy-state wiring
is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter
and supervision docs, OpenCode 2 live fixtures, and regression tests for the
unknown->1.x launch path and the version-gated busy-plugin shape are included.

* no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd

* feat(opencode): launch detected OpenCode 2 workers with --standalone

OpenCode 2's default launch attaches to the shared background service, which
hosts plugins with the service's environment and ignores the pane's
OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions,
FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end

OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory
and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never
found its root or delivered a prompt. Interrupted and failed turns now end a
turn like a succeeded one, while a server shutdown does not.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e

A private opencode serve plus a loopback provider proves the five primary
plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection
guards, turn-end guard, watcher arming across succeeded, interrupted, and
failed turns. The fixture declares the complete model capabilities object,
which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs
it across several OpenCode 2 executables. The credentialed half now picks its
run flags and session tables by detected major, so it keeps working on 1.x.

Fixture ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent

OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it.
After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts.

* docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes

Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate.

* no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit

* no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence

* no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation

* no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs

* test(opencode): verify OpenCode 1.x live with the dual-export plugins

Pin the live e2e TUI panes to the resolved opencode executable, and record
OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the
turn-end guard, watcher continuity, and 1.x worker launch and exit.

* no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs

* no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes

---------

Co-authored-by: Niranjan <nniranc@gmail.com>
Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(bin): add opt-in local Claude usage threshold warner

A usage window is exhausted without warning: work is cut off mid-run
rather than stopping deliberately, and nothing says a window is close to
its cap until it is already gone.

bin/fm-usage-warner.sh is a threshold warner, not a usage viewer.
quota-axi is already the one Claude-quota reader this repo depends on
(bin/fm-quota-axi-lib.sh), so the warner runs that existing read rather
than adding a second reader that could drift from it: one bounded
`quota-axi --provider claude --json --full --no-credential-refresh`, the
snapshot validated with fm_quota_json_valid, and the provider row picked
with the same FM_QUOTA_ROW_JQ join the dispatch consumers use, under
schema 5 and schema 6 alike.

Thresholds are `<window-id>:<percent>` directives in config/usage-warner,
naming quota-axi's own window ids (five_hour, seven_day, model:<name>)
rather than a second vocabulary, and compared against each window's own
percentUsed. Naming the window is the point: the session and weekly
windows exhaust at very different times, so a figure that does not say
which window is close misleads.

One read with two callers and no new schedule: `check` is what a human
runs on demand and what the watcher runs on its ordinary check-shim
cadence once `arm` registers state/usage-warner.check.sh through
bin/fm-check-register.sh. There is no daemon.

Warnings post through macOS Notification Center with the same osascript
call bin/fm-supervise-daemon.sh's wedge alarm makes, without sourcing or
coupling to that daemon's away-mode-specific config and rate limiting.
Delivery is edge-triggered against state/.usage-warner: a crossing warns
once, stays quiet while it holds, and re-arms when the window next reads
back below its threshold, the same standing-report contract
fm-mail-check.sh and fm-tool-update-check.sh already use.

bin/fm-procevent-quota.sh already reads the same snapshot, so docs name
the boundary rather than leaving two similar surfaces unexplained. That
source fires at most once, wakes firstmate with a durable check event,
and compares a provider's aggregate effectivePercentRemaining. This
warner is the standing, human-facing half: per-window, delivered to the
person through Notification Center, and self-re-arming. Neither replaces
the other and arming both is reasonable.

Opt-in is load-bearing and enforced in code. With no valid directive,
`check` returns before any read and `arm` refuses, so an unconfigured
home gains no behaviour change, no new failure mode, and no new hard
dependency at dispatch. Notification Center is macOS-only, so `arm`
refuses on other platforms instead of registering a check that could
never speak.

The warner only warns. It does not stop or pace dispatch, and its
coverage is exactly what quota-axi can see locally, so a plan or
organisation spend cap quota-axi does not report is not covered; the doc
says so rather than implying coverage it does not have.

The new test script carries no measured CI duration hint yet, so the
portable-serial lane balances it on the conservative default until the
hints are next refreshed from a green run, per docs/fm-test-portable-shards.md.

* no-mistakes(review): Key usage-warner de-dupe on window and threshold; fix docs

* no-mistakes(review): Keep absent-window crossings recorded; stub osascript in tests

* no-mistakes(document): Document absent-window de-dupe carry-forward in usage-warner header

* no-mistakes(review): Serialize usage-warner check, dedupe directives, drop vacuous greps

* no-mistakes(document): List usage-warner check lock in state layout

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Kostadin Pop-Kochev <kostadin.popkocev@gmail.com>
Co-authored-by: Niranjan <nniranc@gmail.com>
Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(bin): use clock-stable /proc starttime for remote job worker ownership

Linux remote job ownership compared ps lstart, which drifts on WSL2 while
/proc starttime stays fixed, so ensure treated a healthy worker as dead and
started another supervisor on every call. fm_remote_job_process_start now emits
a proc-starttime token when /proc is readable and falls back to ps lstart
otherwise. The Linux start path hands a live worker of the configured root
whose lock still records a legacy start over to one replacement, checked
against the recorded and live command so no unrelated pid is signaled.

Fixes kunchenguid#2921

* no-mistakes(review): Treat legacy lstart claim starts as indeterminate, not mismatched

* no-mistakes(test): Assert legacy handoff interrupts job once

* no-mistakes(test): Remove scratch debug test; legacy job case git-tracked

* no-mistakes(test): Revert legacy start comparator; simplify legacy handoff test

* fix(bin): drop automatic legacy worker upgrade; document manual one-time restart

* chore: remove stray file

* no-mistakes(document): Drop stale handoff mention from remote worker docs

* fix(bin): verify pre-update remote job workers via legacy lstart

When a recorded start lacks the proc-starttime= prefix, compare it against
ps -o lstart= without LC_ALL=C so a healthy pre-update worker still verifies.
The ordinary code-identity path then replaces it once instead of piling
supervisors. Add regression coverage for the bare-lstart cookie and the
lock upgrade path.

* no-mistakes(document): Clarify legacy remote-job start verification docs

* no-mistakes(review): Preserve C-locale legacy cookie verification, captain

* no-mistakes(document): Clarify legacy process identity cookie compatibility

* no-mistakes(lint): Replace deprecated fgrep with lint-compliant grep -F

---------

Co-authored-by: Pitch Perfekt Collective <pitchperfekt@Pitchs-Mac-Studio.local>
Co-authored-by: mehulbhagwani <270960355+mehulbhagwani@users.noreply.github.com>
…ude (kunchenguid#6722)

* fix: use managed-compatible worker approval modes

* no-mistakes(document): update codex and claude worker approval mode docs

* no-mistakes(ci): Fixed two test files that weren't updated when the PR changed the default claude permission mode from `--dangerously-skip-permissions` (bypass) to `--permission-mode auto`. Changes: - `tests/fm-claude-trust.test.sh` lines 664 and 696: updated two `assert_grep` assertions from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` - `tests/fm-backend-orca.test.sh` line 568: updated one `assert_contains` assertion from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` Both tests now pass locally (orca: exit=0, backend-dispatch/claude-trust: exit=0)

* fix: remove stray scratch directories from tests/

tests/.tmp-managed-approvals-test.bBuGhY/ and tests/.tmp-managed-approvals2.exz2qr/
were accidentally committed as worktree-local scratch dirs and do not belong in the repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve bypass defaults with persistent managed approval failover

* no-mistakes(review): fix approve-for-me fixture and bypass diagnostic messages

* no-mistakes(review): restore absent-Claude-default test and fix pass messages

* fix: recognize managed Codex approval prompts in the C locale

* no-mistakes(review): Fix managed approval failover bounds, authorization, inheritance, and capture

* Simplify managed approval recovery to persistent explicit failover

* no-mistakes(review): Guard failover authorization and unlink staging before recovery

* no-mistakes(document): Correct managed approval documentation and consolidate owner references

* no-mistakes(document): Correct managed-approval references and stale hook guarantees

* fix: require approval and inherit managed modes from primary

* no-mistakes(document): Clarify live hook verification scope

* no-mistakes(review): Remove dead approval_configured key from relaunch metadata

* fix: resurface queued wakes from handling successors (kunchenguid#7466)

* fix(bin): resurface wakes appended while a handling successor is parked

The supervision host parks on the handling successor it starts for a wake
its engine handled, and a handling successor never resurfaced recovery, so
a row another script appended during the park (a captain inbox note from
the Basecamp integration) sat queued until an unrelated close - four hours
in the observed case.

A handling successor now records the newest queued sequence at its start
and closes once with the recovery wake while a newer row is still queued;
a newer row already consumed only advances that mark, so the predecessor's
delivered episode is still never re-announced.

Follow-ups, not built here:
- A row appended in the short window between a handled close and its
  successor's start (about a second) is below the successor's mark; if the
  engine handles that close without claiming the row, the row waits for the
  next close. Closing that window needs the host to treat rows left queued
  after a handled attended turn as main's, which is a separate host change.
- After a machine reboot the primary session does not restart on its own,
  so queued wakes wait until it is resumed by hand. Either auto-resume the
  primary session after boot, or alert the owner when the primary session
  is down while wakes are queued.

* no-mistakes(review): Close handling-successor startup wake race

* no-mistakes(test): Stabilize parked-host inbox wake regression timeout

* no-mistakes(document): Clarify handling-successor wake continuity guarantees

* no-mistakes(ci): Stabilized the flaky remote secondmate lifecycle test by extending the auto-relaunch watcher deadline from ~30 to ~60 seconds. The CI runner exceeded the old deadline by about 3 seconds; the changed production branch was not reachable in this fixture. `bash -n` and `git diff --check` pass. The full test was attempted but exceeded the 600-second local execution limit before reaching this case

* no-mistakes(ci): Fixed flaky concurrent Herdr recovery testing by retrying only documented session-lock contention after both competitors finish. Cleanup now handles fixture-owned read-only hooks. Targeted E2E passed; bash syntax and diff checks passed. Shellcheck reported only existing SC1091 source-following notices

* no-mistakes(test): Preserve successor wake boundaries across handoffs

* no-mistakes(test): Revert successor boundaries and drain teardown fixture wake

* no-mistakes(test): Notify main when downtime handback publication fails

* no-mistakes(test): Revert downtime handback notification changes

* no-mistakes(document): Correct stale supervision wake routing documentation

* fix(bin): stop the supervision host deadlocking and re-announcing handed-back closes

Two defects in this branch's handling-successor changes surfaced as the
"Behavior portable serial 2" shard hanging at its 30-minute limit.

A TERM that lands while the host settles a handled turn's successor handoff
left the host holding the wake-queue and recovery-marker locks; its exit
cleanup then waited forever on the queue lock it still owned. The cleanup now
releases those locks if this process owns them before releasing its grant.

When an accepted close turns main-only at its turn, the host hands it back and
leaves the successor running, but republished the episode as pending
downtime, so that successor re-announced the same close and exited. The
handback now republishes the episode as already announced; a later durable
append still opens a pending episode the successor recovers.

* no-mistakes(test): Park on successor after handling settlement failure

* no-mistakes(test): Use tab-delimited handled-settlement failure logs

---------

Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com>

* fix(bin): preserve accents in pending-reply summaries (kunchenguid#7618)

* fix(bin): keep accented letters in pending-reply request summaries

fm_pending_reply_summarize deleted every byte outside printable ASCII, so a Portuguese summary lost its accents: "não faça a correção" was stored as "no faa a correo". It now removes control characters only. The 120-character cap counts UTF-8 characters from lead bytes, so it never splits a letter under any caller locale, including the C locale.

* no-mistakes(document): Documentar preservação de acentos nos resumos

---------

Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com>

* feat: add OpenRouter support for typed dispatch resolution (kunchenguid#7149)

* Add explicit OpenRouter Jev dispatch resolution

* no-mistakes(document): Clarify dispatch documentation and remove stale duplicates

* chore: exclude local Stele binding from PR

* no-mistakes(document): Correct dispatch help and never-send documentation

* ci: retrigger CI after a timed-out test shard

* no-mistakes(document): Correct dispatch architecture documentation ownership

* feat(bin): publish lane capacity facts with beat pressure and keyed limits (kunchenguid#6983)

Opt-in: fm-watch.sh publishes state/lane-capacity.json via bin/fm-capacity.sh --publish only while config/lane-capacity exists; adds beat pressure and keyed limits to fm-capacity.sh. Unconfigured fm-capacity output unchanged.

* fix(bin): propagate contribution input assembly failures (kunchenguid#5359)

* fix(bin): transport contribution-input JSON through slurpfile, not argjson

bin/fm-fleet-snapshot.sh --contribution-input passed the full backlog and
task-record JSON to jq through --argjson. A single --argjson argument is
capped by the kernel's MAX_ARG_STRLEN (131072 bytes on Linux), so a large
enough backlog made the kernel refuse to exec jq while the script still
exited 0 with a broken result.

Transport both documents through temporary files and --slurpfile, matching
the pattern the default snapshot path already uses, and propagate a
non-zero exit when that jq invocation fails.

Fixes kunchenguid#5349

* test: narrow contribution-input regression to final assembly failure

The ARG_MAX transport is already fixed on main by kunchenguid#6830, so keep only the regression proving a failing final jq assembly exits non-zero.

* fix: correct Rex secondmate grouping and lifecycle (kunchenguid#7656)

* Fix Rex secondmate session lifecycle

* no-mistakes(review): Retire Rex secondmate sessions under the owning home identity

* no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches

* no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation

* feat(opencode): add OpenCode 2 support with version-gated launch, plugins, and exit (kunchenguid#7336)

* feat(opencode): add OpenCode 2 support with version-gated launch and plugins

OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.*
plugins now export a default definition with an id and a setup function
alongside their legacy OpenCode 1 server export, backed by a new shared
lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check,
pretool-check, and sessionstart-nudge install their behavior from setup(ctx)
on OpenCode 2 and keep the server entrypoint for OpenCode 1.

fm-spawn probes the resolved opencode executable's --version once and branches
the launch, model flag, reasoning config, exit key, and busy-state plugin off
the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model,
the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the
session.execution.* busy plugin); OpenCode 1.x and any undetected or
unparseable version fall back to the 1.x shape (--model, 1.x config, /exit)
everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1
keeps the pre-port session.status/session.idle plugin so its busy-state wiring
is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter
and supervision docs, OpenCode 2 live fixtures, and regression tests for the
unknown->1.x launch path and the version-gated busy-plugin shape are included.

* no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd

* feat(opencode): launch detected OpenCode 2 workers with --standalone

OpenCode 2's default launch attaches to the shared background service, which
hosts plugins with the service's environment and ignores the pane's
OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions,
FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end

OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory
and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never
found its root or delivered a prompt. Interrupted and failed turns now end a
turn like a succeeded one, while a server shutdown does not.

Ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e

A private opencode serve plus a loopback provider proves the five primary
plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection
guards, turn-end guard, watcher arming across succeeded, interrupted, and
failed turns. The fixture declares the complete model capabilities object,
which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs
it across several OpenCode 2 executables. The credentialed half now picks its
run flags and session tables by detected major, so it keeps working on 1.x.

Fixture ported from kunchenguid#5143.

Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>

* feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent

OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it.
After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts.

* docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes

Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate.

* no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit

* no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence

* no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation

* no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs

* test(opencode): verify OpenCode 1.x live with the dual-export plugins

Pin the live e2e TUI panes to the resolved opencode executable, and record
OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the
turn-end guard, watcher continuity, and 1.x worker launch and exit.

* no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs

* no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes

---------

Co-authored-by: Niranjan <nniranc@gmail.com>
Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(bin): resolve the tmux container from firstmate's own session, not the most recent group member (kunchenguid#7662)

* fix(bin): resolve the tmux container from the caller's own session, not the most recent group member

* no-mistakes(test): Fall back when the $TMUX session id is stale

* no-mistakes(document): Document tmux container choice under grouped sessions

* fix(bin): read a fully dim Cursor idle placeholder as an empty composer (kunchenguid#7647)

* fix(bin): read a fully dim Cursor placeholder as an empty composer

cursor-agent 2026.10.19 draws the composer glyph and its idle placeholder
as one SGR 2 run with no reverse-video cell, so ghost stripping empties the
row and the empty-content guard returned unknown. Relaunch and exit then
refused on every idle Cursor pane on tmux.

When a styled bare row strips to empty and the plain row is an agent glyph
followed by an anchored idle placeholder, read it empty. Bright typed text,
other dim text, and unstyled rows keep their existing verdicts.

Fixes kunchenguid#7413

* docs(verification): record the fully dim Cursor placeholder verdict and refresh command

* fix(bin): say why away-mode inject deferred and pin named Claude idle composer as empty (kunchenguid#6163)

* fix(bin): recognize a named Claude session's titled composer rule on cursorless backends

`claude -n <name>` right-aligns the session name in the rule above the
composer. Cursorless backends (Herdr) accepted only solid rules, so the
scan saw the lower rule alone, the idle composer read `unknown`, and
away-mode delivery to a named Claude primary deferred until return.

A rule carrying one right-aligned title now opens a separator pair. It
never closes one, the pair it opens counts only over an agent-glyph row,
and it is never a valid Pi pair, so Pi's verdict is unchanged.

The away-mode composer deferral now names which refusal it was:
unsubmitted text, or no composer shape recognized.

* no-mistakes(review): Reword composer deferral reason to cover busy harness

* no-mistakes(ci): ci-3 (Greptile, user chose to fix): The rule that must always hold is that Claude's titled opening rule is accepted for any non-empty session name that doesn't start with a space and doesn't contain the rule glyph `─`, including a multiword name. The only place that check lives is the title `case` in `_fm_composer_titled_rule_row` (bin/fm-composer-lib.sh). It now reads `case "$title" in ''|' '*|*─*) return 1 ;; esac`, as the user asked, and the comment above the function mentions multiword titles. I added regression fixtures to `test_matrix_claude_named_session_titled_rule` in tests/fm-composer-lib.test.sh, next to the existing single-word `Andrew` cases. They use the title `Andrew Chen` and check that an idle composer on herdr reads `empty` and a composer with typed text reads `pending`. Something you should know: the old pattern already accepted a title with a space in the middle. It only rejected a title ending in a space. So the new multiword fixtures also pass against the old code, and the bot's failure scenario did not actually happen for "Andrew Chen". What the fix really changes is that it drops the trailing-space rejection, as instructed. Checks run: tests/fm-composer-lib.test.sh and tests/fm-composer-ghost.test.sh both pass (exit 0, no failures), and `shellcheck -x` is clean on both changed files

* no-mistakes(review): Match unnamed-ghost fixture rule width to the partner rule

* fix: use managed-compatible worker approval modes

* no-mistakes(document): update codex and claude worker approval mode docs

* no-mistakes(ci): Fixed two test files that weren't updated when the PR changed the default claude permission mode from `--dangerously-skip-permissions` (bypass) to `--permission-mode auto`. Changes: - `tests/fm-claude-trust.test.sh` lines 664 and 696: updated two `assert_grep` assertions from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` - `tests/fm-backend-orca.test.sh` line 568: updated one `assert_contains` assertion from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` Both tests now pass locally (orca: exit=0, backend-dispatch/claude-trust: exit=0)

* fix: remove stray scratch directories from tests/

tests/.tmp-managed-approvals-test.bBuGhY/ and tests/.tmp-managed-approvals2.exz2qr/
were accidentally committed as worktree-local scratch dirs and do not belong in the repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve bypass defaults with persistent managed approval failover

* no-mistakes(review): fix approve-for-me fixture and bypass diagnostic messages

* no-mistakes(review): restore absent-Claude-default test and fix pass messages

* fix: recognize managed Codex approval prompts in the C locale

* no-mistakes(review): Fix managed approval failover bounds, authorization, inheritance, and capture

* Simplify managed approval recovery to persistent explicit failover

* no-mistakes(review): Guard failover authorization and unlink staging before recovery

* no-mistakes(document): Correct managed approval documentation and consolidate owner references

* no-mistakes(document): Correct managed-approval references and stale hook guarantees

* fix: require approval and inherit managed modes from primary

* no-mistakes(document): Clarify live hook verification scope

* no-mistakes(review): Remove dead approval_configured key from relaunch metadata

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Caleb LeNoir <caleb.lenoir@constructease.com>
Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com>
Co-authored-by: Alexandre Custodio <xandy.alexandrec@gmail.com>
Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com>
Co-authored-by: Pedro Guimarães <21346846+0x7067@users.noreply.github.com>
Co-authored-by: Konner Moshier <39997550+Moshik21@users.noreply.github.com>
Co-authored-by: Oscar Santis Brante <49914973+osantis@users.noreply.github.com>
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Kostadin Pop-Kochev <kostadin.popkocev@gmail.com>
Co-authored-by: Niranjan <nniranc@gmail.com>
Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com>
Co-authored-by: Tomas Perez <71987424+tomasperezcoscia@users.noreply.github.com>
Co-authored-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Co-authored-by: Brian <bthome@users.noreply.github.com>
…ounter loss (kunchenguid#6467)

* fix: notify handling successors about newer inbox wakes

* no-mistakes(document): Document handling-successor inbox regression in watcher coverage list

* no-mistakes(ci): Fixed Greptile P1 (comment 4172295712): a handling successor could miss a new inbox wake when `.wake-queue.seq` was missing or malformed. What must always hold: a row added after the successor took its snapshot must be announced and must never be consumed by an older acknowledgement. Two places were breaking this: 1. `bin/fm-watch.sh`: the successor only recorded the highest inherited sequence number. If the counter was reset, a new row could get a number at or below that and be treated as already covered. This now stays silent forever once the inherited rows are acknowledged and the queue drains. 2. `bin/fm-wake-lib.sh` `fm_wake_append_locked`: a reset counter reused the sequence number of a row still in the queue. The predecessor's pending `--ack-through` consumes rows by sequence number, so it deleted the new row before anyone handled it. This was a gap in the existing allocator, and it is what made the before-ack cases fail even after fix 1. Fixes: - Watcher: the successor now snapshots the inherited rows themselves instead of their highest sequence. On each poll it announces if any queued row is not in that snapshot. It still never consumes rows, and inherited rows alone stay silent. - Allocator: the next sequence is now one more than the larger of the counter and the highest sequence still queued, so a new row can't land inside an outstanding acknowledgement's range. - `docs/watcher-continuity.md`: the paragraph and coverage list are updated to match. Tests: `test_append_wakes_handling_successor` in `tests/fm-watch-arm.test.sh` now inherits two rows and takes a counter state (intact, missing, malformed). The four new cases are after-ack and before-ack with a missing or malformed counter, and they run real watcher, arm and drain processes. In after-ack the queue is fully drained before the new append. Evidence: - Before any production change (on 92c724d), all four new cases failed: "not ok - handling successor stranded the second inbox wake" for after-ack and before-ack. - With only the watcher fix, the after-ack cases passed and the before-ack cases still failed, which shows the allocator fix is needed. - With both fixes, all four pass. Full results: `fm-watch-arm` 36/36 (including the intact after-ack and before-ack cases, so ordinary silence and fresh-beacon delivery still hold), `fm-watch-recovery-loop` 2/2, `fm-inbox` 16/16, `fm-pi-watch-extension` 60/60, `fm-wake-queue` 67/67, and all four `fm-wake-drain-*` suites passed (9, 9, 18, 14). Canonical `bin/fm-lint.sh` with `PATH=/home/nino/.local/bin:$PATH` passed: ShellCheck clean, actionlint 1.7.12, 3 workflows valid. That lint run was in local changed-file mode with ShellCheck source following disabled. In a first run of all nine suites in parallel, `fm-wake-queue` failed on "presentation lock delayed the drain for 10s" and one `fm-pi-watch-extension` timing test failed. Both passed in full when rerun on their own, so I read that as load contention, not this change. Design decision to flag: the allocator change sits in the shared wake library, so it goes beyond the watcher comparison the bot pointed at. It is a one-expression change, and the before-ack tests can't pass without it, because otherwise the predecessor's acknowledgement deletes the new message. Not done, per the phase boundary: nothing committed, pushed or posted, and the live fleet was not touched. Evidence logs are in /tmp/fmci, not in a pipeline logs/evidence directory

* no-mistakes(ci): Fixed both Greptile findings on PR kunchenguid#6467. Nothing was committed, pushed or posted, and the live fleet was not touched. **ci-1 (4172409813): a new wake was mistaken for an inherited one.** - Invariant that must hold: a successor treats a row as covered only if it is an unchanged inherited row in the inherited recovery episode. - Cause: after the predecessor's acknowledgement and a lost counter, a re-append with the same kind, key, payload and epoch was byte-identical to the snapshot row, so the successor stayed silent. - Fix (`bin/fm-watch.sh`): I used the existing recovery generation rather than a new identity scheme. Any append after an acknowledgement mints a new generation. The successor records the generation with its row snapshot, under the queue lock and in arm-check's lock order. Each poll counts the queue as newer if a row is not in the snapshot, or if the queue is non-empty and the generation changed. The existing arm-check then decides whether to announce. No rows are consumed, and inherited rows alone stay silent. - The row snapshot is still needed for before-ack, because an append before the acknowledgement keeps the same generation. **ci-2 (4172409821): large sequence numbers became unusable.** - Invariant that must hold: sequence numbers are always exact digit-only decimals. - Fix (`bin/fm-wake-lib.sh`): the allocator picks the largest of the counter and the queued sequences by comparing decimal text (length, then string order, leading zeros stripped), then adds 1 in bash with `$((10#$seq + 1))`. This restores the 64-bit exactness of the original bash counter. - Same fix at two places in `bin/fm-wake-drain.sh` that print sequences: `presented_max_row` and `ACK_THROUGH` print the matched text instead of `max + 0`. **Tests** - `tests/fm-watch-arm.test.sh`: two new `identical` cases, after-ack with a missing and with a malformed counter. A fake `date` pins the epoch, and the test asserts the row is byte-identical, then checks timely notification, preservation until its own acknowledgement, and no repeat notification. - `tests/fm-wake-queue.test.sh`: new `test_large_sequences_stay_exact_decimal_identifiers` starts from 2147483647, 9007199254740991 and 9223372036854775806, each with an intact, missing and malformed counter. It checks the next sequence is exact and digit-only. For the first two it also checks a real drain shows both rows, names the exact acknowledgement sequence, and acknowledging empties the queue. - `docs/watcher-continuity.md` now describes the generation check. **Evidence** (in `/home/nino/.no-mistakes/evidence/01M40CDTX2000A5TFFRZN27YFR/ci-round3/`) - Before, on 050a5fc: both identical cases failed with "handling successor stranded the second inbox wake (after-ack)". The large-sequence test failed with "advanced to '9223372036854775808', not 9223372036854775807". - After the fix, all three pass (`after-*.log`). - All nine suites passed, each run on its own, all with 0 not ok (`final-results.txt`): `fm-wake-queue` 68/68, `fm-watch-arm` 38/38, `fm-watch-recovery-loop` 2/2, `fm-inbox` 16/16, `fm-pi-watch-extension` 60/60, `fm-wake-drain-open-decisions-cursor` 9/9, `fm-wake-drain-open-decisions` 9/9, `fm-wake-drain-outcome-backstop` 18/18, `fm-wake-drain-unread-status` 14/14. - `bin/fm-lint.sh` with `PATH=/home/nino/.local/bin:$PATH` passes: ShellCheck clean, actionlint 1.7.12, 3 workflows valid (local changed-file mode). **Not run** - The only OpenCode suite is an opt-in live test, so I did not run it. **Limits** - The 2.14748e+09 symptom doesn't reproduce with the awks here (gawk, mawk 1.3.4, busybox). The failure reproduced locally is the precision loss at 64-bit values. - The drain's awk comparisons are only exact up to 2^53. That predates this PR and I left it alone, so the drain test stops at 2^53

* no-mistakes(document): Reattach successor poll-loop sentence to its subject
… directory is removed (kunchenguid#7685)

* fix(tests): bound lock waits on removed dirs and reap fixture-root processes

A lock wait spun every 0.1s forever once its directory was removed, so a
watcher or reconcile scan of a deleted test home never reached its own
home-gone exit. fm_lock_acquire_wait now returns within 0.2s once the
lock's parent directory is gone.

Test teardown now signals every process naming the exact fixture root
(TERM, then KILL) before removing it, in both fm_test_cleanup and the
orphan sweep. The supervision-host fake harness sessions stop once their
home is gone or the suite pid has died, so a hard-killed runner no longer
leaves them polling under init.

Fixes kunchenguid#7672

* fix(tests): bound the session-lock concurrency barrier

Forty workers polled a find pipeline every 0.01s to wait for each other,
so the barrier alone launched thousands of processes a second, and once a
stopped run removed the barrier directory the workers polled forever under
init. Count entries with a builtin glob every 0.05s, and end the wait once
the directory is gone, the suite has died, or the stub block bound passes.

Refs kunchenguid#7672
The merged fork and upstream tests no longer pack into nine shards under the 20-minute budget.
The fork's archive-configuration regressions need a quoted value that contains a hash and a spaced table header to read as tasks-axi reads them.
An answered legacy row cannot vouch for a call whose own row was archived unanswered.
The fork's inbox grace replaced the stuck-busy escalation that the upstream live-proof text describes.
@cloud-practitioner cloud-practitioner changed the title feat(bin): integrate upstream runtime and supervision updates Merge upstream firstmate into the fork Oct 24, 2026
@cloud-practitioner
cloud-practitioner merged commit d544f73 into main Oct 25, 2026
@cloud-practitioner

Copy link
Copy Markdown
Owner Author

Before and after test runs

Test runs of seven suites on fork main before the merge and on the PR head after the merge. Each run is bash tests/<suite>.test.sh, one suite at a time, with the Herdr, FM_* and forge credential variables unset. Only non-live suites ran. A pink box marks the line that matters in each screenshot.

  • Before: fork main 244d8368, an exported copy of the tree before the merge.
  • After: the PR head e0112f67, the final head of this PR.
  • Four suites are fork-only or fork-touched and must pass on both sides: the PR gate, the secondmate stand-down, the Bitbucket CI-ready checks and the brief suite. The captain-hold suite exists on both sides and gains cases. The wedge-cap and Rex backend suites are new in this merge, so the before run shows that the suite is missing.
  • The data is synthetic. The capture tool masks secret-shaped text, so a few short strings show as ***masked***. The Rex run pipes its output through tr to replace angle brackets, because the masker blanked the whole transcript.

PR gate suite (fork-only, kept)

The PR gate suite tests a fork-only check. It must pass before the merge and after the merge.

Before (fork main 244d836) - Marked line: the suite passes before the merge.

Before: fm-pr-evidence on fork main

After (PR head e0112f6) - Marked line: the suite passes after the merge.

After: fm-pr-evidence on the PR head

Secondmate stand-down suite (fork-only, kept)

The secondmate stand-down suite tests a fork-only rule. It must pass before the merge and after the merge.

Before (fork main 244d836) - Marked line: the suite passes before the merge.

Before: fm-secondmate-dormant on fork main

After (PR head e0112f6) - Marked line: the suite passes after the merge.

After: fm-secondmate-dormant on the PR head

Bitbucket CI-ready and named-head suite (fork-only, kept)

The Bitbucket CI-ready checks are fork-only. They must pass before the merge and after the merge.

Before (fork main 244d836) - Marked line: the suite passes before the merge.

Before: fm-dod-lib on fork main

After (PR head e0112f6) - Marked line: the suite passes after the merge.

After: fm-dod-lib on the PR head

Brief generation suite (fork and upstream text)

The brief suite tests fork text and upstream changes. It must pass before the merge and after the merge.

Before (fork main 244d836) - Marked line: the suite passes before the merge.

Before: fm-brief on fork main

After (PR head e0112f6) - Marked line: the suite passes after the merge.

After: fm-brief on the PR head

Captain-hold lifecycle suite (upstream adds cases)

The merge adds new captain-hold cases. The suite must pass before the merge and after the merge.

Before (fork main 244d836) - Marked line: the suite passes before the merge.

Before: fm-captain-hold-lifecycle on fork main

After (PR head e0112f6) - Marked lines: the merge adds these test cases.

After: fm-captain-hold-lifecycle on the PR head

Watcher wedge-cap suite (new from upstream)

The wedge-cap suite comes from the upstream merge. It is new in the merge.

Before (fork main 244d836) - Marked lines: the run finds no suite.

Before: fm-watch-wedge-cap on fork main

After (PR head e0112f6) - Marked line: the new suite passes after the merge.

After: fm-watch-wedge-cap on the PR head

Rex backend suite (new from upstream)

The Rex backend suite comes from the upstream merge. It is new in the merge.

Before (fork main 244d836) - Marked lines: the run finds no suite.

Before: fm-backend-rex on fork main

After (PR head e0112f6) - Marked line: the new suite passes after the merge.

After: fm-backend-rex on the PR head

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.