Skip to content

feat(bin): notify owned PR conflicts and failing required checks behind an opt-in flag - #7634

Merged
kunchenguid merged 8 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/upstream-7588-pr-health
Oct 22, 2026
Merged

kunchenguid merged 8 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/upstream-7588-pr-health

Conversation

@tiago-peixoto

@tiago-peixoto tiago-peixoto commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7588.

Notifications apply only when config/pr-health-monitor is set.

Live verification of the no-required-checks fallback

The fallback is restored to satisfy issue #7588 and match bin/fm-pr-state.sh.
When no reported check is required, reported failures count.
When a required check has reported, optional failures remain excluded.
All other review fixes and the contribution-observer arming call remain unchanged.

This read-only run used #7208 at latest commit 0648e4a371156b819b2c40e9b078c6cc6535e5f7.
GitHub returned 19 reported checks, zero required checks, a genuine FAILURE for PR must be raised via no-mistakes, and a CANCELLED serial test.
The same latest commit and check conditions were observed before and after the fix.
The test enrolled the public PR only in an isolated local home and ran the real monitor against GitHub.
No PR, check, branch, comment, or review was changed on GitHub.
A forwarding gh shim recorded the actual read requests and responses without replacing any forge result.

Before the fix, at 2026-10-22T13:53:11Z:

$ fm-pr-state.sh https://github.com/kunchenguid/firstmate/pull/7208
CHECKS: no required check has reported; readiness unconfirmed
REPORTED CHECK: Behavior portable serial 2 (CANCELLED)
REPORTED CHECK: PR must be raised via no-mistakes (FAILURE)

$ fm-pr-health.sh poll
Output bytes: 0

After restoring the fallback, at 2026-10-22T13:56:37Z:

$ fm-pr-health.sh poll
pr-health: https://github.com/kunchenguid/firstmate/pull/7208 has failing checks at latest commit 0648e4a: Behavior portable serial 2, PR must be raised via no-mistakes (task live-fallback)

$ fm-pr-health.sh poll
Output bytes: 0

The first poll now notifies about the reported failures, while an unchanged second poll stays silent.
The deterministic regressions also verify that a later failing required check at the same commit still notifies once.

Intent

Address #7588: owned pull requests raise no notification when required checks fail or a definite merge conflict appears after the ready report.
Firstmate should notify once per condition per latest commit when an owned PR has a failing required check or a definite conflict, treat unknown mergeability as neither clean nor conflicting, and only notify, never rebase or fix on its own.
The notification is opt-in: it stays off unless a home enables it, and the default behaviour is unchanged.
When the base branch requires no checks, a failing reported check still counts, as bin/fm-pr-state.sh already does for stacked pull requests.

What Changed

  • Add bin/fm-pr-health.sh (poll and arm), gated by the new local config/pr-health-monitor presence flag: it enrolls the PRs this home owns (recorded pr= values, durable contribution records, and the open PR on a live task copy's current branch), reads each open PR's REST mergeability and GraphQL check rollup, and prints one wake line per new condition - a definite conflict once per latest commit, and a failing required check once per latest commit and check name, where any failing reported check counts when no check is marked required and null mergeability is treated as neither clean nor conflicting. Fingerprints live in state/pr-health.json; bin/fm-bootstrap.sh arms the check at session start only when the flag is present, and the monitor never comments on or changes a PR.
  • Extend bin/fm-contributions.sh with read-only owned (JSON array of owned PR URLs with tasks and final state) and enroll (records a task's PR URL as an unobserved record, no-op when already recorded, refused when retired), and move the branch-to-PR lookup from bin/fm-teardown.sh into bin/fm-pr-lib.sh as fm_pr_number_from_branch so teardown and the health monitor share it.
  • Add tests/fm-pr-health.test.sh (registered in the pr-forge family in bin/fm-test-run.sh), extend tests/fm-bootstrap.test.sh for the flag-gated arming, and document the flag and script in docs/configuration.md, docs/scripts.md, docs/fm-test-isolation-proof.md, and the operational-home-layout skill.

Risk Assessment

✅ Low: The feature is opt-in and notify-only, the round-4 delta is a ten-line restoration of the fallback the intent requires with semantics identical to fm-pr-state.sh, every prior recorded fix (status-line exclusion, per-name dedupe, final-cache refresh, retirement refusal, unreadable-record pause) is still present and covered by behavior tests through the registered check, and the only open point is a human confirmation that the intent supersedes the round-3 removal decision.

Testing

Ran the focused monitor, PR-state and bootstrap test files (all passing), then drove the real monitor, the pre-fix script and the real watcher from disposable lab homes against real GitHub pull requests with a logging forwarding gh shim: the opt-in default, the no-required-checks fallback on PR 7208, once-per-commit dedupe, a definite conflict on PR 7612, one durable wake, and read-only forge access all behaved as intended, with transcripts, extracted GitHub observations and the fingerprint file saved as evidence. Bootstrap arming, required-check gating, UNKNOWN mergeability, retirement and unreadable-record behavior were not validated live and are reported as untested: they were exercised only through the existing fake-forge and bootstrap test suites because the repository has no required checks on main and GitHub's unknown mergeability cannot be held on demand.

  • Live validation: ✅ go - 7 of 11 scenarios driven live against the product
Scenario Result Live Evidence
With config/pr-health-monitor absent, poll prints nothing and reads nothing, and arm refuses, leaving state and data untouched ✅ pass live live/transcript.txt (flag absent section: poll exit 0 with no output, arm exit 1 with refusal message, 'gh calls so far: 0', 'state and data unchanged')
An owned PR whose base requires no checks and has failing reported checks raises one notification naming the failing checks at the latest commit ✅ pass live live/transcript.txt poll-1 line for PR 7208 at 0648e4a; live/observations.json shows all 19 contexts isRequired=false with two failing (FAILURE and CANCELLED)
The same failing checks at the same latest commit do not notify again on the next poll ✅ pass live live/transcript.txt poll-2 and poll-4 print nothing; before-after/transcript.txt third poll silent
The pre-fix monitor printed nothing for the same PR; the target commit prints the fallback line once (regression reproduced live) ✅ pass live before-after/transcript.txt
An owned PR with a definite merge conflict (REST mergeable=false) raises one conflict notification per latest commit and no repeat ✅ pass live live/transcript.txt poll-3 conflict line for PR 7612 at 4c60946, poll-4 silent; live/observations.json rest_mergeable=false
The watcher turns a new condition into exactly one durable wake and a second sweep raises none ✅ pass live watcher/transcript.txt: one pr-health row in state/.wake-queue, second run 'no actionable wake' exit 124
The monitor only reads the forge: every GitHub call is a REST GET of one PR or a GraphQL query, never a mutation, comment, or branch change ✅ pass live live/observations.json call_kinds and live/gh-calls.log (12 calls); live/responses/*.cmd
Bootstrap arms state/pr-health.check.sh only when config/pr-health-monitor exists ⏸️ untested no The prior run did not drive the real bootstrap against a live home with and without config/pr-health-monitor; it only ran tests/fm-bootstrap.test.sh (bootstrap-tests.log), which is a unit test and doe…
Once a required check has reported, an optional failure does not notify, while a required failure at an already reported commit still wakes once ⏸️ untested no Not driven live. kunchenguid/firstmate has no required status checks on main (the rules endpoint returns none and every live context reports isRequired=false), so no real PR with a required check exis…
Unknown mergeability (REST mergeable=null) is neither clean nor conflicting and never clears a reported conflict ⏸️ untested no Not driven live. GitHub returns null mergeability only transiently while it recomputes, which cannot be held on demand without pushing to a real PR; the live PR 7612 reached the monitor already comput…
A retired contribution is not re-owned by cached or fresh branch discovery, and an unreadable contribution record pauses the monitor once without dropping fingerprints ⏸️ untested no Not driven live. Branch discovery needs a live task copy whose branch has an open PR in this account's repository, which the prior run did not set up. Only the fake-forge cases in tests/fm-pr-health.t…
Evidence: Evidence index

Source: Evidence index

# Test-step evidence for fm/upstream-7588-pr-health (target a986889b)

All live runs used a disposable lab home made by bin/fm-lab-home.sh under /tmp and removed afterwards.
GitHub was only read: every forwarded gh call was a REST GET of one pull request or a GraphQL query (live/observations.json, live/responses/*.cmd).
No PR, branch, check, review or comment was changed.

- live/transcript.txt: flag absent (poll silent, arm refuses, zero gh calls, state untouched); flag on + enroll PR 7208 (19 reported contexts, none required, two failing) -> one "has failing checks" line, second poll silent; enroll conflicting PR 7612 -> one conflict line, PR 7208 silent, next poll silent.
- live/observations.json: latest commit, isRequired values, failing contexts and mergeable per PR, extracted from the logged real GitHub responses.
- live/pr-health-after-7612.json: the fingerprint file after the runs.
- before-after/transcript.txt: the pre-fix script (3bc134e6) polling PR 7208 prints nothing; the target script in the same home then prints the fallback line once.
- watcher/transcript.txt: the real watcher (fm-watch-checkpoint.sh) turns the conflict on PR 7612 into one wake-queue row; a second run finds no new wake.
- focused-tests.log: bin/fm-test-run.sh tests/fm-pr-health.test.sh tests/fm-pr-state.test.sh (fake forge, all 23 monitor cases).
- bootstrap-tests.log: bin/fm-test-run.sh tests/fm-bootstrap.test.sh (bootstrap arms the monitor only with the flag).
Evidence: Live monitor transcript (flag off, fallback on PR 7208, conflict on PR 7612)

Source: Live monitor transcript (flag off, fallback on PR 7208, conflict on PR 7612)

$ bash state/pr-health.check.sh pr-health: #7208 has failing checks at latest commit 0648e4a: Behavior portable serial 2, PR must be raised via no-mistakes (task ship1) [exit 0] $ bash state/pr-health.check.sh [exit 0] $ fm-contributions.sh enroll conflict1 #7612 bash state/pr-health.check.sh pr-health: #7612 conflicts with its base at latest commit 4c60946 (task conflict1) [exit 0] $ bash state/pr-health.check.sh [exit 0]

started 2026-10-22T15:21:45Z
== Scenario: flag absent (default off) ==

$ ~/.no-mistakes/worktrees/5284051b2355/01M5J2MS3JJ7PRQ8D02NK7JSS2/bin/fm-pr-health.sh poll
[exit 0]

$ ~/.no-mistakes/worktrees/5284051b2355/01M5J2MS3JJ7PRQ8D02NK7JSS2/bin/fm-pr-health.sh arm
stderr: fm-pr-health: config/pr-health-monitor is absent; the PR health monitor stays off
[exit 1]
gh calls so far: 0
state and data unchanged
== Scenario: opt in, enroll PR with failing reported checks and no required contexts ==

$ touch /tmp/fm-lab.wf712R/config/pr-health-monitor
[exit 0]

$ ~/.no-mistakes/worktrees/5284051b2355/01M5J2MS3JJ7PRQ8D02NK7JSS2/bin/fm-contributions.sh enroll ship1 https://github.com/kunchenguid/firstmate/pull/7208
[exit 0]

$ ~/.no-mistakes/worktrees/5284051b2355/01M5J2MS3JJ7PRQ8D02NK7JSS2/bin/fm-pr-health.sh arm
registered: state/pr-health.check.sh
[exit 0]
contributions.check-trust
contributions.check.sh
pr-health.check-trust
pr-health.check.sh

$ bash /tmp/fm-lab.wf712R/state/pr-health.check.sh
pr-health: https://github.com/kunchenguid/firstmate/pull/7208 has failing checks at latest commit 0648e4a: Behavior portable serial 2, PR must be raised via no-mistakes (task ship1)
[exit 0]

$ bash /tmp/fm-lab.wf712R/state/pr-health.check.sh
[exit 0]
== Scenario: enroll a PR with a definite conflict ==

$ ~/.no-mistakes/worktrees/5284051b2355/01M5J2MS3JJ7PRQ8D02NK7JSS2/bin/fm-contributions.sh enroll conflict1 https://github.com/kunchenguid/firstmate/pull/7612
[exit 0]

$ bash /tmp/fm-lab.wf712R/state/pr-health.check.sh
pr-health: https://github.com/kunchenguid/firstmate/pull/7612 conflicts with its base at latest commit 4c60946 (task conflict1)
[exit 0]

$ bash /tmp/fm-lab.wf712R/state/pr-health.check.sh
[exit 0]
finished 2026-10-22T15:21:52Z
== gh calls ==
api repos/kunchenguid/firstmate/pulls/7208 api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F numb

api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F number=7208 -f query=query($owner:String!,$repo

api repos/kunchenguid/firstmate/pulls/7208 
api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F number=7208 -f query=query($owner:String!,$repo
api repos/kunchenguid/firstmate/pulls/7612 
api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F number=7612 -f query=query($owner:String!,$repo
api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F number=7208 -f query=query($owner:String!,$repo

api repos/kunchenguid/firstmate/pulls/7612 
api graphql --paginate --slurp -F owner=kunchenguid -F repo=firstmate -F number=7612 -f query=query($owner:String!,$repo
== tmux calls (expect none) ==
Evidence: Real GitHub observations extracted from logged responses

Source: Real GitHub observations extracted from logged responses

pr7208: head 0648e4a371156b819b2c40e9b078c6cc6535e5f7, 19 contexts, isRequired values [False], failing: PR must be raised via no-mistakes (FAILURE), Behavior portable serial 2 (CANCELLED), mergeable true pr7612: head 4c60946a388715d1cfb0a65865f4c2193b0ca0c1, mergeable false, 21 contexts, none required, none failing call kinds: GraphQL query, REST GET single PR (12 calls, no mutations)

{
 "calls": [
  {
   "kind": "GraphQL query",
   "pr": "7208"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7208"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7208"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7612"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7208"
  },
  {
   "kind": "GraphQL query",
   "pr": "7208"
  },
  {
   "kind": "GraphQL query",
   "pr": "7208"
  },
  {
   "kind": "GraphQL query",
   "pr": "7208"
  },
  {
   "kind": "GraphQL query",
   "pr": "7612"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7208"
  },
  {
   "kind": "GraphQL query",
   "pr": "7612"
  },
  {
   "kind": "REST GET single PR",
   "pr": "7612"
  }
 ],
 "pr7208": {
  "gql_head": "0648e4a371156b819b2c40e9b078c6cc6535e5f7",
  "contexts": 19,
  "isRequired_values": [
   "False"
  ],
  "failing": [
   {
    "name": "PR must be raised via no-mistakes",
    "conclusion": "FAILURE",
    "startedAt": "2026-10-22T02:22:08Z"
   },
   {
    "name": "Behavior portable serial 2",
    "conclusion": "CANCELLED",
    "startedAt": "2026-10-22T03:06:13Z"
   }
  ],
  "rest_state": "open",
  "rest_merged_at": null,
  "rest_mergeable": true,
  "rest_head": "0648e4a371156b819b2c40e9b078c6cc6535e5f7"
 },
 "pr7612": {
  "rest_state": "open",
  "rest_merged_at": null,
  "rest_mergeable": false,
  "rest_head": "4c60946a388715d1cfb0a65865f4c2193b0ca0c1",
  "gql_head": "4c60946a388715d1cfb0a65865f4c2193b0ca0c1",
  "contexts": 21,
  "isRequired_values": [
   "False"
  ],
  "failing": []
 },
 "call_kinds": [
  "GraphQL query",
  "REST GET single PR"
 ]
}
Evidence: Live before/after: pre-fix script silent, target script notifies once

Source: Live before/after: pre-fix script silent, target script notifies once

$ [pre-fix 3bc134e6] fm-pr-health.sh poll [exit 0] $ [target a986889b] bin/fm-pr-health.sh poll pr-health: #7208 has failing checks at latest commit 0648e4a: Behavior portable serial 2, PR must be raised via no-mistakes (task ship1) [exit 0] $ [target a986889b] bin/fm-pr-health.sh poll (again, expect silence) [exit 0]

pre-fix script: 3bc134e6 no-mistakes(review): refuse retired re-enrollment, require marked checks, verify owned records
<       | ([$ctx[] | select(.required and .failing) | .name] | unique) as $failing
>       | any($ctx[]; .required) as $has_required
>       | ([$ctx[] | select(.failing and (.required or ($has_required | not))) | .name] | unique) as $failing
<             | "pr-health: \($url) has failing required checks \($where): \($new_failing[:3] | join(", "))\(if ($new_failing | length) > 3 then " and \(($new_failing | length) - 3) more" else "" end) (task \($owners))")])}
>             | "pr-health: \($url) has failing \(if $has_required then "required " else "" end)checks \($where): \($new_failing[:3] | join(", "))\(if ($new_failing | length) > 3 then " and \(($new_failing | length) - 3) more" else "" end) (task \($owners))")])}

$ [pre-fix 3bc134e6] fm-pr-health.sh poll  (same lab home, PR 7208 enrolled, flag on)
[exit 0]
state/pr-health.json after pre-fix poll:
{
  "schema": "fm-pr-health.v1",
  "prs": {
    "https://github.com/kunchenguid/firstmate/pull/7208": {
      "conflicts": [],
      "checks": []
    }
  },
  "branches": {},
  "ownership_error": false
}

$ [target a986889b] bin/fm-pr-health.sh poll  (same home, same fingerprint file)
pr-health: https://github.com/kunchenguid/firstmate/pull/7208 has failing checks at latest commit 0648e4a: Behavior portable serial 2, PR must be raised via no-mistakes (task ship1)
[exit 0]
$ [target a986889b] bin/fm-pr-health.sh poll  (again, expect silence)
[exit 0]
captured 2026-10-22T15:22:58Z
Evidence: Real watcher turns the live conflict into one wake-queue row

Source: Real watcher turns the live conflict into one wake-queue row

started 2026-10-22T15:23:14Z
$ fm-contributions.sh enroll conflict1 https://github.com/kunchenguid/firstmate/pull/7612
[exit 0]
$ fm-pr-health.sh arm
registered: state/pr-health.check.sh
[exit 0]
$ FM_POLL=1 FM_SIGNAL_GRACE=0 FM_CHECK_INTERVAL=0 FM_HEARTBEAT=999999 fm-watch-checkpoint.sh --seconds 20
check: /tmp/fm-lab.H49vqo/state/pr-health.check.sh: pr-health: https://github.com/kunchenguid/firstmate/pull/7612 conflicts with its base at latest commit 4c60946 (task conflict1)
[exit 0]
state/.wake-queue rows:
1792682598	1	check	contribution-8099a3da003185826467257c0ae2fd4ccd5a2b1054989bb4d971fc078da371d7	check: contributions conflict1 8099a3da003185826467257c0ae2fd4ccd5a2b1054989bb4d971fc078da371d7
1792682598	2	check	contribution-ab0447e4990618966c9eb8d9b00be22f3d776493a9fc692f002b3b89e1e713ad	check: contributions conflict1 ab0447e4990618966c9eb8d9b00be22f3d776493a9fc692f002b3b89e1e713ad
1792682599	3	check	/tmp/fm-lab.H49vqo/state/pr-health.check.sh	check: /tmp/fm-lab.H49vqo/state/pr-health.check.sh: pr-health: https://github.com/kunchenguid/firstmate/pull/7612 conflicts with its base at latest commit 4c60946 (task conflict1)
$ second watcher run with the same state (expect timeout 124: no new wake)
checkpoint: no actionable wake within 8s
[exit 124]
tmux calls:
finished 2026-10-22T15:23:28Z
Evidence: Fingerprint file after the live runs

Source: Fingerprint file after the live runs

{
  "schema": "fm-pr-health.v1",
  "prs": {
    "https://github.com/kunchenguid/firstmate/pull/7208": {
      "conflicts": [],
      "checks": [
        {
          "head": "0648e4a371156b819b2c40e9b078c6cc6535e5f7",
          "name": "Behavior portable serial 2"
        },
        {
          "head": "0648e4a371156b819b2c40e9b078c6cc6535e5f7",
          "name": "PR must be raised via no-mistakes"
        }
      ]
    },
    "https://github.com/kunchenguid/firstmate/pull/7612": {
      "conflicts": [
        "4c60946a388715d1cfb0a65865f4c2193b0ca0c1"
      ],
      "checks": []
    }
  },
  "branches": {},
  "ownership_error": false
}
Evidence: Focused test run (fm-pr-health, fm-pr-state)

Source: Focused test run (fm-pr-health, fm-pr-state)

FM_TEST_BEGIN 2026-10-22T15:19:30Z tests/fm-pr-health.test.sh family=pr-forge expected_gate_skip=none
FM_TEST_BEGIN 2026-10-22T15:19:30Z tests/fm-pr-state.test.sh family=pr-forge expected_gate_skip=none
ok - a passing required check and a skipped one leave nothing to report
ok - a terminal pull request reports that state and nothing else
ok - draft state blocks readiness
ok - stale changes-requested verdicts explain a blocking review decision
ok - approved PR ignores stale changes-requested history
ok - current changes-requested review blocks readiness
ok - a CHANGES_REQUESTED decision is always reported
ok - the author's own review is never listed as a blocker
ok - a pending approval is not reported as a blocker
ok - required failure blocks readiness
ok - given gh's sentence, an unreported required check is unconfirmed, other check lookup failures refuse
ok - given gh's sentence, a head with no reported checks is unverified rather than ready
ok - a failing reported check is shown when the base has no required checks
ok - a pending reported check is shown when the base has no required checks, and a failed fallback read refuses
ok - a protected base reports only required checks
ok - help states what empty output means and what is out of scope
ok - unknown and conflicting mergeability block readiness
ok - argument and lookup refusals exit nonzero
FM_TEST_END 2026-10-22T15:19:31Z tests/fm-pr-state.test.sh exit=0 duration_ms=873 gate_skip=false
ok - UNKNOWN then DIRTY wakes once
ok - DIRTY seen again at the same commits wakes nothing
ok - a conflict notifies once per latest commit despite recovery or base movement
ok - each required check that fails on an adopted draft wakes once per latest commit
ok - a fallback notification does not hide a later required failure at the same commit
ok - each check is judged by its newest run
ok - a latest commit that moves between the two reads waits for the next pass
ok - a red check on another home's PR wakes nothing in this home
ok - a PR enrolled before any ready report, and one kept after cleanup, both still wake
ok - an open PR that replaces a closed one on the same task branch is discovered
ok - a green PR without a reviewer never notifies
ok - a base with no required checks counts a failing reported check
ok - a retired contribution is not reclaimed through the cached branch lookup
ok - a retired contribution is not reclaimed by a fresh branch lookup
ok - an unreadable durable record pauses the monitor once and keeps its fingerprints
ok - nothing prints while nothing changed
ok - a merged PR leaves the monitor
ok - an absent flag leaves the home untouched
ok - the watcher turns one conflict into one durable wake
ok - check notifications survive cleanup and repeat only at a new latest commit
ok - all owned PR reads start in one wave without rotating a backlog
ok - all check pages contribute to required-check classification
ok - GitLab ownership and branch discovery make no GitHub reads
FM_TEST_END 2026-10-22T15:20:24Z tests/fm-pr-health.test.sh exit=0 duration_ms=54403 gate_skip=false
FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0 duration_ms=54625
FM_TEST_SUMMARY_FAMILY family=pr-forge count=2 duration_ms=55276 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-pr-health.test.sh duration_ms=54403
FM_TEST_SLOWEST rank=2 script=tests/fm-pr-state.test.sh duration_ms=873
exit=0
Evidence: Bootstrap test run

Source: Bootstrap test run

FM_TEST_BEGIN 2026-10-22T15:22:15Z tests/fm-bootstrap.test.sh family=session-bootstrap expected_gate_skip=none
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
ok - bootstrap reports treehouse lease + tasks-axi/quota-axi bootstrap contracts
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
ok - bootstrap enforces no-mistakes minimum version
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
ok - bootstrap enforces gh-axi minimum version
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS

... [25212 bytes truncated] ...

atal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
ok - bootstrap gates resolver fields and additive harnesses on the typed key
FM_TEST_END 2026-10-22T15:23:44Z tests/fm-bootstrap.test.sh exit=0 duration_ms=89031 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=89128
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=1 duration_ms=89031 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-bootstrap.test.sh duration_ms=89031
exit=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-pr-health.sh:146 - Commit a986889 restores the no-required-checks fallback ($has_required at bin/fm-pr-health.sh:145-146, the unlabelled "has failing checks" wording at :158, docs/configuration.md:632-633, and the two tests at tests/fm-pr-health.test.sh:227-243 and :385-401) that the round-3 recorded decision any-failing-check-fallback-beyond-intent asked to remove and that commit 3bc134e removed. The current user intent explicitly requires it ("When the base branch requires no checks, a failing reported check still counts, as bin/fm-pr-state.sh already does for stacked pull requests"), the issue text says "At least these changes count", and the commit is the author's own later act, so I treat the intent as the later human decision and did not change the code. The implementation matches fm-pr-state.sh's rule exactly: the fallback applies when no reported context is marked required, and once any required context has reported, optional failures are ignored; dedupe per latest commit and check name is unchanged, and a required failure appearing after a fallback notification at the same commit still wakes (traced through the optional-then-required test sequence). This finding only asks the supervisor to confirm the reversal of the round-3 decision; no code change is recommended.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 11 scenarios driven live against the product
Scenario Result Live Evidence
With config/pr-health-monitor absent, poll prints nothing and reads nothing, and arm refuses, leaving state and data untouched ✅ pass live live/transcript.txt (flag absent section: poll exit 0 with no output, arm exit 1 with refusal message, 'gh calls so far: 0', 'state and data unchanged')
An owned PR whose base requires no checks and has failing reported checks raises one notification naming the failing checks at the latest commit ✅ pass live live/transcript.txt poll-1 line for PR 7208 at 0648e4a; live/observations.json shows all 19 contexts isRequired=false with two failing (FAILURE and CANCELLED)
The same failing checks at the same latest commit do not notify again on the next poll ✅ pass live live/transcript.txt poll-2 and poll-4 print nothing; before-after/transcript.txt third poll silent
The pre-fix monitor printed nothing for the same PR; the target commit prints the fallback line once (regression reproduced live) ✅ pass live before-after/transcript.txt
An owned PR with a definite merge conflict (REST mergeable=false) raises one conflict notification per latest commit and no repeat ✅ pass live live/transcript.txt poll-3 conflict line for PR 7612 at 4c60946, poll-4 silent; live/observations.json rest_mergeable=false
The watcher turns a new condition into exactly one durable wake and a second sweep raises none ✅ pass live watcher/transcript.txt: one pr-health row in state/.wake-queue, second run 'no actionable wake' exit 124
The monitor only reads the forge: every GitHub call is a REST GET of one PR or a GraphQL query, never a mutation, comment, or branch change ✅ pass live live/observations.json call_kinds and live/gh-calls.log (12 calls); live/responses/*.cmd
Bootstrap arms state/pr-health.check.sh only when config/pr-health-monitor exists ⏸️ untested no The prior run did not drive the real bootstrap against a live home with and without config/pr-health-monitor; it only ran tests/fm-bootstrap.test.sh (bootstrap-tests.log), which is a unit test and doe…
Once a required check has reported, an optional failure does not notify, while a required failure at an already reported commit still wakes once ⏸️ untested no Not driven live. kunchenguid/firstmate has no required status checks on main (the rules endpoint returns none and every live context reports isRequired=false), so no real PR with a required check exis…
Unknown mergeability (REST mergeable=null) is neither clean nor conflicting and never clears a reported conflict ⏸️ untested no Not driven live. GitHub returns null mergeability only transiently while it recomputes, which cannot be held on demand without pushing to a real PR; the live PR 7612 reached the monitor already comput…
A retired contribution is not re-owned by cached or fresh branch discovery, and an unreadable contribution record pauses the monitor once without dropping fingerprints ⏸️ untested no Not driven live. Branch discovery needs a live task copy whose branch has an open PR in this account's repository, which the prior run did not set up. Only the fake-forge cases in tests/fm-pr-health.t…
  • MISE_TRUSTED_CONFIG_PATHS=~ MISE_DATA_DIR=~/.local/share/mise bin/fm-test-run.sh tests/fm-pr-health.test.sh tests/fm-pr-state.test.sh (total=2 failed=0)
  • MISE_TRUSTED_CONFIG_PATHS=~ MISE_DATA_DIR=~/.local/share/mise bin/fm-test-run.sh tests/fm-bootstrap.test.sh (total=1 failed=0, includes test_pr_health_monitor_arms_only_with_its_flag)
  • Live lab home (bin/fm-lab-home.sh create) with a forwarding gh shim that logs every call and response: bin/fm-pr-health.sh poll and arm with config/pr-health-monitor absent -> silent, arm refused, zero gh calls, state/data unchanged
  • Live: bin/fm-contributions.sh enroll ship1 https://github.com/kunchenguid/firstmate/pull/7208, bin/fm-pr-health.sh arm, then bash state/pr-health.check.sh twice -> one fallback line naming both failing reported checks at 0648e4a, then silence
  • Live: bin/fm-contributions.sh enroll conflict1 https://github.com/kunchenguid/firstmate/pull/7612 then two polls -> one conflict line at 4c60946, PR 7208 silent, then silence
  • Live before/after: pre-fix git show 3bc134e6:bin/fm-pr-health.sh poll on PR 7208 printed nothing; target script poll in the same home printed the fallback line once, third poll silent
  • Live: FM_POLL=1 FM_SIGNAL_GRACE=0 FM_CHECK_INTERVAL=0 FM_HEARTBEAT=999999 bin/fm-watch-checkpoint.sh --seconds 20 in a lab home with PR 7612 enrolled -> one pr-health wake-queue row; rerun with FM_WATCH_HANDLING_SUCCESSOR=1 exits 124 with no new wake
  • Verified from logged real responses (live/observations.json): all 12 gh calls are REST GET single-PR or GraphQL query reads; isRequired=false for all 19 contexts on PR 7208 and all 21 on PR 7612; GraphQL headRefOid equals REST head.sha
  • Read-only GitHub probes: gh api repos/kunchenguid/firstmate/rules/branches/main returns no required_status_checks rule, so no real PR with a required check exists in this repository for a live required-check scenario
⚠️ **Document** - 1 info
  • ℹ️ docs/fm-test-isolation-proof.md:164 - tests/fm-pr-health.test.sh joined the pr-forge family in bin/fm-test-run.sh, and script_allows_concurrency grants concurrency by family membership alone, so the new script runs concurrently with the other eight members without an isolation proof covering it. The doc now states that the 2026-09-12 result does not cover it, matching how the two earlier late joiners were handled. A follow-up could re-run bin/fm-test-isolation-proof.sh --pool pr-forge --jobs 4 at nine members on an idle host and record the result; that is a verification run, not a documentation edit, so it is out of this phase's scope.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thank you @tiago-peixoto. This is a careful piece of work: the monitor is read-only, it stays fully off unless config/pr-health-monitor exists, and the live forge transcripts make the conflict and ownership paths easy to trust.

What I checked (head 2cefd90e against main 9fe25bca):

  • Opt-in holds. With the flag absent, poll exits before any read or write, arm refuses, and bootstrap arms nothing. The other default-path edits are a behavior-neutral move of the branch-to-PR lookup into fm_pr_number_from_branch (teardown still calls it with --state all inside the task copy) and two new fm-contributions.sh subcommands (owned, enroll) that nothing calls unless the monitor runs. The brief waiting-rule change the review flagged isn't in the diff anymore.
  • Security: clean. Forge calls are REST GETs and GraphQL queries only. There are no .github changes, and no secrets or new network endpoints.
  • The attestation matches 2cefd90e, and PR must be raised via no-mistakes passes. CI is green except Behavior portable serial 2, which was cancelled at the 30-minute cap (fix(pi): keep the Pi watcher armed after a successor or arm child dies #7255). I've re-run that job.

Contract-class: opt-in (my own read). VISION.md per rule: One captain aligns (one wake per condition, nothing while unchanged) · Authority aligns (notify only, never rebases or fixes, off unless enabled) · Scripts aligns (deterministic fingerprints in a script) · Restart aligns (fingerprints and enrolment are durable) · Delegation aligns · Vendor aligns (GitHub-exposed isRequired/mergeable semantics) · Scope aligns (field issue #7588).

One thing I need from you before this can land (waiting on you):
The PR says Fixes #7588, but it deliberately drops one of that issue's quality criteria: "When the base branch requires no checks, a failing reported check still counts, as bin/fm-pr-state.sh already does for stacked pull requests." The tip only counts contexts with isRequired == true (bin/fm-pr-health.sh ~L149), and docs/configuration.md now says a non-required check never counts "even when the base requires no check at all". On a repo with no required checks (this one included), a red PR would never wake. Could you pick one?

  1. Restore the no-required-checks fallback that Owned pull requests raise no notification when required checks fail or a merge conflict appears after the ready report #7588 asks for. A nice side effect: the failing-check path becomes live-testable right here, since this repo requires no checks. Please drive that scenario live and post the evidence, or re-raise through git push no-mistakes so it's captured.
  2. Keep the narrower rule, but change Fixes #7588 to Refs #7588 and say in the body which criterion is left open, so the issue stays open for it.

The remaining review note, that arm always arms the contribution observer, sits inside the opt-in path. It's fine to leave as is, or trim if you agree with it. No rush, and thanks again.

@tiago-peixoto
tiago-peixoto force-pushed the fm/upstream-7588-pr-health branch from 2cefd90 to b0115bc Compare October 22, 2026 15:30
@tiago-peixoto tiago-peixoto changed the title feat(bin): notify owned PR health changes behind an opt-in flag feat(bin): notify owned PR conflicts and failing required checks behind an opt-in flag Oct 22, 2026
@kunchenguid
kunchenguid merged commit c79cd40 into kunchenguid:main Oct 22, 2026
21 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @tiago-peixoto — really appreciate you taking the time on this.

You picked option 1: the no-required-checks fallback is back, matching fm-pr-state.sh, and you drove it live against a real PR, including the before/after regression. With that, #7588 is fully covered. The monitor stays off unless config/pr-health-monitor exists.

roderik added a commit to roderik/firstmate-rex that referenced this pull request Oct 23, 2026
…ex integrations (#11)

* feat: add auto-detected Rex session backend (kunchenguid#7633)

* 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

* fix(bin): guard the wedge cap-fire counter reset with the cap-state rollback (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

* fix(bin): restore watcher downtime before main-only hand-back (kunchenguid#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

* test(herdr): bound the piped composer probe with fm_run_timed instead of GNU timeout (kunchenguid#7636)

* feat: add Rex push notifications and cursor-aware composer detection (kunchenguid#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

* feat(bin): notify owned PR conflicts and failing required checks behind 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

* test: isolate real-Rex suites on private servers (kunchenguid#7649)

* 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: distinguish outdated tools from missing tools (kunchenguid#5688)

* 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

* fix(bin): clarify ahoy skill legacy session-start exclusion to require verbatim match (kunchenguid#7530)

* Clarify ahoy legacy startup exclusion

* Fix quoted payload code span

* fix(bin): resolve timeout/gtimeout to executable paths via type -P (kunchenguid#7637)

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

* no-mistakes(review): Remove redundant slashless runner resolution from external timeout

* fix(bin): count answered captain holds moved to the Done archive as durable (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

* fix(bin): read a PR with no CI checks as unverified, not checks green (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>

* fix(bin): correct the wedge reset-failure log line and make the fault 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: 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

* docs: describe the merged Rex spawn gate and guide contents

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Lorenzo Minghini <LorenzoMinghini@users.noreply.github.com>
Co-authored-by: zachlandes <zlandes@gmail.com>
Co-authored-by: Tiago <tiagop@hey.com>
Co-authored-by: YifuGu <39033099+ironerumi@users.noreply.github.com>
Co-authored-by: laffy-999 <l4ffy999@gmail.com>
Co-authored-by: chsong <116850020+chsong1@users.noreply.github.com>
Co-authored-by: Hamish <89876653+FrankieShoe@users.noreply.github.com>
Co-authored-by: Harlan Do <hieudtr8@gmail.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>
tiago-peixoto added a commit to tiago-peixoto/firstmate that referenced this pull request Oct 25, 2026
* feat(bin): add fm-capacity.sh to report open lanes and host headroom (#6973)

Adds bin/fm-capacity.sh, a read-only, network-free report of open ship/scout lanes against an optional config/lane-capacity target plus host load, memory and disk headroom. Without config/lane-capacity it reports counts only and gives no dispatch-hold advice. Docs, operational-home-layout skill entry, a gated AGENTS.md pointer, and tests/fm-capacity.test.sh.

* fix(bin): bound repeat stale alarms for unknown-but-alive progressing panes (#7437)

* fix: bound unknown-live watcher alarms

* no-mistakes(review): Gate unknown-alive stale bound on progress; fix arm CONFIG

* no-mistakes(review): Keep watch.env loading only in the watcher arm

* no-mistakes(review): Seed worktree progress before first unknown-alive alarm in test

* no-mistakes(document): List config/watch.env in operational home layout

* no-mistakes(review): Narrow watch.env docs to arm-launched watchers

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* fix(bin): honor explicit no-run verdict in crew-state (#5888)

* fix(bin): honour the no-run verdict from axi status in crew-state

* no-mistakes(review): Honor authoritative no-run verdict across fallbacks

* no-mistakes(lint): Quote no-run sentinel to satisfy ShellCheck

* no-mistakes(document): document authoritative no-run verdict in crew-state attribution

* fix(composer): recognize Grok approval-titled borders (#6132)

* fix(composer): accept Grok 1.0.44 approval-titled bottom border as empty composer

* no-mistakes(document): Document Grok’s aligned approval title

* no-mistakes(ci): Captain, removed the unused test variable that failed Lint 1. ShellCheck, the composer suite, and the remote provisioning test pass locally. Behavior portable serial 9 failed during fixture cloning in an unchanged test, before composer behavior was exercised; no further code change is warranted for that failure

* no-mistakes(review): drop dead approval guard, make Grok fixtures locale-independent widths

* fix(bin): stop shellcheck re-expanding fm-wake-lib in teardown

Lint 1 dies when ShellCheck expands bin/fm-wake-lib.sh twice from
bin/fm-teardown.sh. The top-level source already names that file. Point
the second directive at /dev/null and leave the runtime source unchanged.

* docs: record Grok 1.0.44's approval-titled composer border

The verification record scoped its titled-bottom-border note to the Grok
1.0.5 three-column overhang but never recorded the newer shape this branch
accepts, so a maintainer could not tell the two cases apart or know what a
Grok upgrade re-verifies.

Add a dated record of the 1.0.44 shape - the typed model/effort title plus
its ` · always-approve` suffix, drawn inside a rounded bottom border at the
aligned width - with the live verdicts that prove it, the portable test that
owns it, and the fail-safe verdict every neighbouring title keeps.

* no-mistakes(review): Narrow Grok composer coverage claims to verified matrix

* no-mistakes(document): Correct stale Grok verification notes

* docs: trigger CI re-run for flaky shard 4

Behavior portable serial 4 failed on the previous head with one assertion
only, `not ok - the NUL-bearing line was not captured`, in the unchanged
remote-reply and relay path; its fixture hardcodes /bin/sleep, direct NUL
ingestion passes, and the other 19 checks were green. GitHub refused a job
re-run for this fork PR, so this empty commit is the re-trigger. No content
change.

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

* fix(composer): restrict Grok approval titles to rounded borders

Reject light, heavy, double, and ASCII approval-title boxes at the shared
classifier boundary. Only the rounded family has recorded live evidence,
and an empty verdict grants permission to type into the pane.

Cover denied families through tmux and cursorless backend profiles while
preserving rounded idle, draft, malformed-title, and legacy behavior.

* no-mistakes(document): Refresh Grok composer verification evidence

* no-mistakes(ci): Fixed the stale local link in `docs/verification/runtime-backends.md`; the section heading’s anchor had changed. `tests/fm-documentation-audiences.test.sh` passes

* fix(bin): point non-checkout omp homes at fm <project> relaunch (#7155)

* fix(omp): point non-checkout homes at fm <project> relaunch and name why the extension is not loaded

* no-mistakes(review): Remove unset-FM_HOME omp watch cause line

* feat(fm-watch): bound consecutive wedge escalations behind an opt-in cap (#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

* feat(bin): archive task status history on teardown behind an opt-in flag (#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

* feat(bin): re-raise unanswered PR review threads after an opt-in reply age (#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 fc8972cd 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 fc8972cd. 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

* feat(bin): refuse unguarded Herdr lifecycle briefs at spawn behind an opt-in flag (#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

* fix(bin): withhold a child's PR-ready line until checks green behind an opt-in flag (#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 #6454 and #7277. Rerunning the Herdr job should be enough; fixing the lock budget belongs to those issues, not this PR

* feat: add auto-detected Rex session backend (#7633)

* 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

* fix(bin): guard the wedge cap-fire counter reset with the cap-state rollback (#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

* fix(bin): restore watcher downtime before main-only hand-back (#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

* test(herdr): bound the piped composer probe with fm_run_timed instead of GNU timeout (#7636)

* feat: add Rex push notifications and cursor-aware composer detection (#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

* feat(bin): notify owned PR conflicts and failing required checks behind an opt-in flag (#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

* test: isolate real-Rex suites on private servers (#7649)

* 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: distinguish outdated tools from missing tools (#5688)

* 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

* fix(bin): clarify ahoy skill legacy session-start exclusion to require verbatim match (#7530)

* Clarify ahoy legacy startup exclusion

* Fix quoted payload code span

* fix(bin): resolve timeout/gtimeout to executable paths via type -P (#7637)

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

* no-mistakes(review): Remove redundant slashless runner resolution from external timeout

* fix(bin): count answered captain holds moved to the Done archive as durable (#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

* fix(bin): read a PR with no CI checks as unverified, not checks green (#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" (#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 (#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>

* fix(bin): correct the wedge reset-failure log line and make the fault test root-safe (#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: resurface queued wakes from handling successors (#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 (#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 (#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 (#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 (#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 #5349

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

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

* fix: correct Rex secondmate grouping and lifecycle (#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 (#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 https://github.com/kunchenguid/firstmate/pull/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 https://github.com/kunchenguid/firstmate/pull/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 https://github.com/kunchenguid/firstmate/pull/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 (#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 (#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 #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 (#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(bin): stop watcher arms whose ground is removed and reap Pi test process groups (#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

* feat(bin): defer wedge escalation while a crew's no-mistakes run is validating (#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.

* fix: identify Windows/Git Bash harness sessions for the session lock (#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 #3396

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

* 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 0baf64f71ffea275292a0fd88d033caccc041fc9)

* 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 preci…
fbdo added a commit to fbdo/firstmate that referenced this pull request Oct 28, 2026
* test(tests): stop orphan-storm fixture failing on slow launches (#7451)

* fix(tests): tolerate slow orphan-storm source launches

* no-mistakes(document): docs: pin orphan-storm row to pacing, not lease expiry

* fix: honor gh-resolved publication targets (#7540)

* fix: respect gh-resolved base in publication target selection

* no-mistakes(document): Document gh-resolved publication target selection

* feat(bin): add Paseo as an experimental session backend (#4728)

* feat(bin): add Paseo as an experimental session backend

Modeled on the cmux adapter, with Herdr's one-workspace-per-task UX:
terminal NAME is the routing authority, workspace title is decoration
only. tmux stays the default; Paseo is opt-in via config/backend,
FM_BACKEND=paseo, or PASEO_AGENT_ID/bundle-id auto-detection.

* no-mistakes(document): docs(paseo): register Paseo across backend inventories and lists

* no-mistakes(document): docs(paseo): list paseo in AGENTS.md config/backend entry

* fix(bin): give Paseo one shared workspace per project with a tab per task

Paseo's sidebar is project > workspace > terminal tab. The adapter used
to create one workspace per task, so a fleet showed up as a pile of
sidebar entries. It now mirrors Herdr's container shape: one shared
`firstmate` (or `2ndmate-<id>`) workspace per project, adopted by cwd
and title or created once, with each task as a terminal tab inside it.
Kill closes only the task's tab; the shared workspace is never archived
and the adapter never runs a `paseo project` command, so Paseo reuses
the project registered for the path instead of multiplying projects.

Live findings folded in: the CLI prints an Electron warning on stderr
when firstmate itself runs inside a Paseo agent, so parsed `--json`
calls no longer merge stderr into stdout; `workspace ls` reports the
cwd normalized but not symlink-resolved, so adoption matches the raw,
logical, and physical path; a bare Escape stays pending in zsh's emacs
keymap and fuses with the next key, so the smoke test verifies labels
with Enter and sends Escape last.

The smoke test now owns a throwaway project directory and deletes the
project it registered, so a captain's real sidebar is never touched.
Both paseo suites get a portable-serial duration hint and a `paseo`
family so the coverage guard stays under its unhinted cap.

* test(bin): teach the backend-list suites about paseo

CI serial shard 5 failed because tests/fm-bootstrap.test.sh still
expected the known-backend list to end at cmux. Add paseo to that
expectation and to the sibling lists in the control, trace-context, and
teardown-safety suites (with a valid paseo endpoint record), and scrub
PASEO_AGENT_ID and __CFBundleIdentifier in the bootstrap suite's hermetic
unset so running the tests from inside a Paseo agent cannot flip a
default-backend case onto paseo the way $TMUX or HERDR_ENV would.

* no-mistakes(review): Fix paseo control keys, secondmate fallback, docs, and tests

* no-mistakes(review): Send paseo C-u as raw byte, refuse untokenized keys

* no-mistakes(document): Sync Paseo docs for key tokens, secondmate fallback

* no-mistakes(lint): Use prefix-form PATH in paseo autodetect tests

* fix(bin): make paseo explicit-only and adopt the tab's own workspace

Paseo stamps its markers into every descendant process: an agent exports
PASEO_AGENT_ID, a terminal tab exports PASEO_TERMINAL_ID and
PASEO_WORKSPACE_ID, and both carry __CFBundleIdentifier=sh.paseo.desktop.
A tmux server started from a Paseo tab inherits all of them, so their
presence never proves the captain chose Paseo (the same finding recorded
on #4484 and #3884). fm_backend_detect now ignores every Paseo marker;
paseo is reachable only through config/backend, FM_BACKEND=paseo, or
--backend paseo, exactly like zellij and orca. That also retires the
auto-detected-paseo secondmate fallback to tmux, which only existed
because of auto-detection: an explicit paseo still refuses --secondmate,
and an unconfigured home inside Paseo stays on tmux silently.

Once Paseo has been selected explicitly, PASEO_WORKSPACE_ID is still
useful: a firstmate running inside a Paseo tab adopts that tab's own
workspace as the project's shared workspace when its cwd is the project,
before falling back to the firstmate-title lookup (herdr's
launcher-identity rule). Verified live that CLI-created tabs export
PASEO_WORKSPACE_ID and no PASEO_AGENT_ID.

Docs also note that `paseo run --cwd` is ignored when PASEO_AGENT_ID is
set; the adapter never uses `paseo run`.

* no-mistakes(test): Drop stale Paseo auto-detect mention from fm-spawn help

* no-mistakes(document): Document Paseo own-workspace adoption order

* no-mistakes(ci): Fixed ci-3 with a test-only change. The unconfigured-spawn case in test_secondmate_spawn_refuses_explicit_paseo_only failed on CI with "error: no launch template for harness unknown". CI runners have no agent process above the test, so harness detection returned unknown before the spawn reached the home check. The fix pins the harness with `printf claude > "$dir/config/secondmate-harness"` just before that spawn call, with a two-line comment explaining why (tests/fm-backend-paseo.test.sh). I verified locally that tests/fm-backend-paseo.test.sh passes, including the secondmate test, and that shellcheck is clean on the file. I did not rerun it detached from the agent process chain; your earlier report of it failing without the pin and passing with it is the evidence for that part. As you instructed, I made no change for ci-1 or ci-2 (the fork approval gate) or for the Lint 2 runner shutdown, which was infrastructure

* fix(bin): let the adapter precheck load paseo

Main now checks that a backend's adapter and its sibling libraries are
readable before sourcing it, and refuses any backend it has no sibling
list for. Paseo had no entry, so every paseo call failed to load the
adapter and fell back to 'unknown'. Paseo sources the same siblings as
cmux, so it shares that entry and sources through the checked path.

* no-mistakes(document): Make fm_backend_source adapter-count comment count-free

* test(bin): prove the Pi branch claims paseo stale wakes

The watcher keys a non-Orca stale wake by the task's window= value, and
the Pi supervision branch resolves a stale row to its task only through
window= (Orca's terminal= key is not indexed, which is how Orca stale
wakes ended up waking main). Paseo records
window=<terminal-uuid>:<workspace-uuid>, so pin that a stale row keyed
by that value is claimed by the branch for the right task.

* no-mistakes(document): Note Paseo adapter in composer capture-window config doc

* fix(bin): refuse a paseo spawn when its inventory is unreadable

Paseo allows duplicate workspaces and terminal names, so the adapter
looks up the project's shared workspace and checks the task's terminal
name before creating either. Both lookups treated a failed or unreadable
listing as empty, so a transient daemon error during spawn created a
second workspace for the project or a duplicate task terminal. The spawn
now refuses instead, and creates nothing, when either inventory cannot be
read.

* docs(paseo): note why capture reads the whole scrollback

Paseo caps terminal scrollback at 1024 lines, so reading all of it and
trimming locally stays bounded however long a task runs. A bounded
`--start -<n>` read is not a drop-in replacement: it counts the blank
screen rows below the prompt as the tail, so a quiet terminal captures
as empty.

* no-mistakes(document): Document Paseo spawn refusal on unreadable inventory

* no-mistakes(lint): Rewrite paseo workspace inventory check as explicit if-block

* feat(bin): keep paseo tasks from cluttering the sidebar

Every project a firstmate worked in got its own "firstmate" workspace in
Paseo, and those workspaces and their projects stayed in the sidebar
after the work was done.

When firstmate itself runs inside Paseo, task tabs now go into its own
workspace, whatever the project. A terminal tab names that workspace
through PASEO_WORKSPACE_ID; an agent session only has PASEO_AGENT_ID, so
its workspace is the one live workspace whose folder is the firstmate
home.

Otherwise the per-project shared workspace is still used, and closing
its last task tab archives it and deletes its project when the folder is
one of the home's own clones. Archiving a workspace also archives its
agents and leaves the project listed, so cleanup skips firstmate's own
workspace, workspaces titled by someone else, workspaces with an agent
in their folder, and projects outside the home, and does nothing when an
inventory cannot be read.

* no-mistakes(review): Retire paseo workspace when tab already gone; rename spawn vars

* no-mistakes(document): Align Paseo doc workspace wording with own-workspace placement

* fix(bin): serialize paseo spawns with workspace cleanup

Cleanup checked that a per-project workspace had no tabs, made more
Paseo calls, and then archived it. A spawn could open a tab in that
workspace in between, and the archive would then take a live task's tab
with it.

Spawning (choosing or creating the workspace and opening the tab) and
cleanup now share one home-wide lock, the stale-owner lock from
fm-wake-lib.sh that herdr already uses. A spawn that cannot get it
refuses and cleanup skips. Cleanup now only archives a workspace for one
of the home's own clones, which only this home spawns into, so a
per-home lock covers every spawn that could use it.

* no-mistakes(document): Scope Paseo workspace retirement to home clones in architecture

* test(paseo): fix shellcheck SC2031 lint warning in fm-backend-paseo test

The workspace-lock holder helper read $! after backgrounding the holder,
which ShellCheck's dataflow analysis flags as SC2031 because the helper
only runs inside command substitution. The holder now records its own
PID ($$, the same process $! named) before signalling readiness, and the
helper returns that. No behavior change.

* test(paseo): keep the smoke test from archiving a workspace it did not create

Run from a Paseo tab or agent, PASEO_WORKSPACE_ID/PASEO_AGENT_ID steered
create_task into the caller's own workspace, which cleanup then archived.
The smoke test now unsets both before sourcing the adapter, archives the
workspace create_task returned only while its cwd is the test's own temp
dir, and runs cleanup once. Test-only; assertions are unchanged.

* no-mistakes(document): List paseo in FM_BACKEND and README backend lists

* no-mistakes(document): Add paseo to backend dependency docs and lists

* no-mistakes(document): docs: list paseo among Command Code refused backends

* no-mistakes(document): Note Paseo as unstyled in opencode footer record

* fix(skills): keep recovery-skill triggers inside Claude Code's listing budget (#7545) (#7558)

Claude Code lists skill descriptions within a budget of 1% of the context
window and drops the least-invoked skills' descriptions first, which are
the rarely-loaded recovery skills whose descriptions AGENTS.md section 13
uses as the trigger index.

- Remove the decision-hold-lifecycle redirect stub; nothing on main
  points briefs at it any more (captain-hold-lifecycle owns the policy and
  docs/captain-hold-lifecycle.md still documents the fm-decision-hold.sh
  shim).
- Tighten the longest skill descriptions to their load triggers, keeping
  every trigger. Total description size drops from 11312 to 6142
  characters across the listing.
- Document /doctor, /skill-doctor, skillListingBudgetFraction, and
  skillOverrides "name-only" in the Claude harness adapter notes as
  opt-in, home-local vendor settings.

No session-start budget warning or other default-on surface is added.

* fix(extension): launch lifecycle helpers through verified Bash (#7563)

* fix(extension): launch lifecycle helpers through verified Bash

The extension host spawned bin/fm-procevent.sh directly with
shell:false, which native Windows Node rejects with spawn EFTYPE because
it cannot run a script through its shebang. Resolve an absolute Bash
from $BASH or PATH, verify it with a short probe (on Windows it must
report an MSYS or Cygwin OSTYPE, so WSL bash.exe is skipped), and pass
the script as its first argument. The child keeps shell:false, the
sanitized environment, literal argv, and the existing failure mapping.

Fixes #7559

* no-mistakes(review): Harden lifecycle Bash interpreter verification

* no-mistakes(document): Document lifecycle Bash verification contract

* fix(extension): try exported BASH only on Windows for lifecycle helpers

* feat(bin): add an opt-in no-mistakes home resolver for host reads (#7348)

Sites that run one no-mistakes daemon per isolated workspace keep each
task's runs in a per-workspace NM_HOME that cannot be derived from the
shared gate remote, so crew state, pipeline spend, and teardown read the
wrong home.

When NM_HOME is unset and config/nm-home-resolver names a site-provided
executable, fm-nm-run-lib.sh now calls it with the task copy path and
exports the printed home for that one call. Only host reads (axi status,
the axi overview, axi logs, runs, daemon status, the state database) and
axi abort --run use it. An explicit NM_HOME always wins, and with no file
behaviour is unchanged.

Fixes #7309

* feat(bin): defer wedge escalation for a worker's own open blocked/needs-decision line (#7361)

* feat(bin): let a worker's own open blocked: or needs-decision: line defer wedge escalation behind config/wedge-defer-worker-wait

Opt-in only: absent the flag the wedge ladder is unchanged. Counts only the worker's own newest line (reserved keys excluded via status_own_open_decisions), defers onto the existing bounded recheck with a verb-specific clearing action and no escalation-count increase, and ends on any newer status line or a newer acknowledged inbox record. The away daemon honors the same evidence before escalating an enriched wedge.

Refs #6899

* no-mistakes(document): Document worker-wait deferral in wait-evidence lists

* feat(brief): forbid leading cd in generated scout and ship briefs (#7481)

* feat: support per-mate secondmate runtime pins (#6917)

* feat(bin): pin per-mate secondmate runtime overrides in config/secondmate-harness

A single default pin line gives every secondmate one runtime, so raising the
default pin silently migrates a mate the captain ruled onto its own runtime.
config/secondmate-harness now accepts optional
"mate <secondmate-id>: <harness> [<model>] [<effort>]" override lines, and
fm-harness.sh secondmate, secondmate-model, and secondmate-effort take the
secondmate id as an optional second argument that selects that line.

Spawn, the control-plane relaunch, and the secondmate restart each pass the
mate id they already know, so all three resolve one decision. A malformed pin
file is refused with its line number and prints no token instead of resolving
around it to the default runtime.

* no-mistakes(review): Bypass unused pins during explicit secondmate relaunch

* no-mistakes(document): Correct and consolidate per-mate runtime pin documentation

* fix: preserve legacy secondmate pin parsing without overrides

* fix(bin): keep legacy secondmate pin files opt-in

config/secondmate-harness is documented as "the first non-empty, non-comment
line is parsed". The per-mate override grammar made the whole file strict, so
a second default line or a fourth token started refusing every secondmate
spawn, relaunch, and restart - even in a home that never wrote a mate line.
That changed how an existing, already-valid file is read, so the feature was
not opt-in.

Scope the strict whole-file parse to files that declare at least one
"mate <secondmate-id>:" line. A file with no mate line parses exactly as
before: first non-empty, non-comment line, the rest ignored, a fourth token
ignored, nothing refused. A file that does declare a mate line keeps the
strict parsing, so a duplicate override, a malformed entry, or an extra token
still refuses loudly with its line number.

Coverage: a legacy file with a second default line and a fourth token now
spawns, relaunches, and restarts a mate onto its first line; a declaring file
that is malformed refuses each of those paths with the offending line; and an
explicit --harness/--model/--effort relaunch succeeds whatever the pin file
holds, because the config read stays behind the explicit-harness guard.

* no-mistakes(document): Document per-mate secondmate runtime pins

* no-mistakes(ci): Fixed `tests/fm-backend-herdr-presentation-e2e.test.sh` teardown to restore write permission on fixture directories before removal. This addresses the read-only git-hook cleanup errors in the failed Herdr check. The real-Herdr presentation E2E test passed locally (exit 0)

* feat(bin): add fm-capacity.sh to report open lanes and host headroom (#6973)

Adds bin/fm-capacity.sh, a read-only, network-free report of open ship/scout lanes against an optional config/lane-capacity target plus host load, memory and disk headroom. Without config/lane-capacity it reports counts only and gives no dispatch-hold advice. Docs, operational-home-layout skill entry, a gated AGENTS.md pointer, and tests/fm-capacity.test.sh.

* fix(bin): bound repeat stale alarms for unknown-but-alive progressing panes (#7437)

* fix: bound unknown-live watcher alarms

* no-mistakes(review): Gate unknown-alive stale bound on progress; fix arm CONFIG

* no-mistakes(review): Keep watch.env loading only in the watcher arm

* no-mistakes(review): Seed worktree progress before first unknown-alive alarm in test

* no-mistakes(document): List config/watch.env in operational home layout

* no-mistakes(review): Narrow watch.env docs to arm-launched watchers

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* fix(bin): honor explicit no-run verdict in crew-state (#5888)

* fix(bin): honour the no-run verdict from axi status in crew-state

* no-mistakes(review): Honor authoritative no-run verdict across fallbacks

* no-mistakes(lint): Quote no-run sentinel to satisfy ShellCheck

* no-mistakes(document): document authoritative no-run verdict in crew-state attribution

* fix(composer): recognize Grok approval-titled borders (#6132)

* fix(composer): accept Grok 1.0.44 approval-titled bottom border as empty composer

* no-mistakes(document): Document Grok’s aligned approval title

* no-mistakes(ci): Captain, removed the unused test variable that failed Lint 1. ShellCheck, the composer suite, and the remote provisioning test pass locally. Behavior portable serial 9 failed during fixture cloning in an unchanged test, before composer behavior was exercised; no further code change is warranted for that failure

* no-mistakes(review): drop dead approval guard, make Grok fixtures locale-independent widths

* fix(bin): stop shellcheck re-expanding fm-wake-lib in teardown

Lint 1 dies when ShellCheck expands bin/fm-wake-lib.sh twice from
bin/fm-teardown.sh. The top-level source already names that file. Point
the second directive at /dev/null and leave the runtime source unchanged.

* docs: record Grok 1.0.44's approval-titled composer border

The verification record scoped its titled-bottom-border note to the Grok
1.0.5 three-column overhang but never recorded the newer shape this branch
accepts, so a maintainer could not tell the two cases apart or know what a
Grok upgrade re-verifies.

Add a dated record of the 1.0.44 shape - the typed model/effort title plus
its ` · always-approve` suffix, drawn inside a rounded bottom border at the
aligned width - with the live verdicts that prove it, the portable test that
owns it, and the fail-safe verdict every neighbouring title keeps.

* no-mistakes(review): Narrow Grok composer coverage claims to verified matrix

* no-mistakes(document): Correct stale Grok verification notes

* docs: trigger CI re-run for flaky shard 4

Behavior portable serial 4 failed on the previous head with one assertion
only, `not ok - the NUL-bearing line was not captured`, in the unchanged
remote-reply and relay path; its fixture hardcodes /bin/sleep, direct NUL
ingestion passes, and the other 19 checks were green. GitHub refused a job
re-run for this fork PR, so this empty commit is the re-trigger. No content
change.

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

* fix(composer): restrict Grok approval titles to rounded borders

Reject light, heavy, double, and ASCII approval-title boxes at the shared
classifier boundary. Only the rounded family has recorded live evidence,
and an empty verdict grants permission to type into the pane.

Cover denied families through tmux and cursorless backend profiles while
preserving rounded idle, draft, malformed-title, and legacy behavior.

* no-mistakes(document): Refresh Grok composer verification evidence

* no-mistakes(ci): Fixed the stale local link in `docs/verification/runtime-backends.md`; the section heading’s anchor had changed. `tests/fm-documentation-audiences.test.sh` passes

* fix(bin): point non-checkout omp homes at fm <project> relaunch (#7155)

* fix(omp): point non-checkout homes at fm <project> relaunch and name why the extension is not loaded

* no-mistakes(review): Remove unset-FM_HOME omp watch cause line

* feat(fm-watch): bound consecutive wedge escalations behind an opt-in cap (#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

* feat(bin): archive task status history on teardown behind an opt-in flag (#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

* feat(bin): re-raise unanswered PR review threads after an opt-in reply age (#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 fc8972cd 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 fc8972cd. 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

* feat(bin): refuse unguarded Herdr lifecycle briefs at spawn behind an opt-in flag (#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

* fix(bin): withhold a child's PR-ready line until checks green behind an opt-in flag (#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 #6454 and #7277. Rerunning the Herdr job should be enough; fixing the lock budget belongs to those issues, not this PR

* feat: add auto-detected Rex session backend (#7633)

* 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

* fix(bin): guard the wedge cap-fire counter reset with the cap-state rollback (#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

* fix(bin): restore watcher downtime before main-only hand-back (#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

* test(herdr): bound the piped composer probe with fm_run_timed instead of GNU timeout (#7636)

* feat: add Rex push notifications and cursor-aware composer detection (#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

* feat(bin): notify owned PR conflicts and failing required checks behind an opt-in flag (#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

* test: isolate real-Rex suites on private servers (#7649)

* 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: distinguish outdated tools from missing tools (#5688)

* 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

* fix(bin): clarify ahoy skill legacy session-start exclusion to require verbatim match (#7530)

* Clarify ahoy legacy startup exclusion

* Fix quoted payload code span

* fix(bin): resolve timeout/gtimeout to executable paths via type -P (#7637)

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

* no-mistakes(review): Remove redundant slashless runner resolution from external timeout

* fix(bin): count answered captain holds moved to the Done archive as durable (#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

* fix(bin): read a PR with no CI checks as unverified, not checks green (#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" (#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 (#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>

* fix(bin): correct the wedge reset-failure log line and make the fault test root-safe (#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: resurface queued wakes from handling successors (#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 (#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 (#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 (#6983)

Opt-in: fm-watch.sh publishes state/lane-capacity.json vi…
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.

Owned pull requests raise no notification when required checks fail or a merge conflict appears after the ready report

2 participants