Repository navigation
Merge upstream firstmate into the fork - #67
Conversation
…cap (kunchenguid#7552) * feat(fm-watch): bound consecutive wedge escalations per worker window An unattended watcher escalation loop once burned ~359M tokens in a day: the watcher keeps re-escalating a pane it believes is wedged, and nothing bounded how many times it did that for one worker window. Add an opt-in ceiling on consecutive wedge escalations for a single window. At FM_WEDGE_MAX_ESCALATIONS the watcher publishes one terminal PERMANENTLY-WEDGED wake and writes both cap markers, after which the wedge path short-circuits for that window until FM_CAP_HORIZON_SECS elapses or an operator removes both markers. A pane hash change alone does not re-engage while the window-scoped marker stands, so a pane that re-renders on every poll cannot rebuild the counter and re-fire every bound. A failed marker write rolls the cap state back rather than amplifying one write failure into a wake-queue flood, and a rollback that itself fails leaves a transient sentinel that holds the wedge path quiet for FM_ROLLBACK_SENTINEL_TTL_SECS. FM_WEDGE_MAX_ESCALATIONS defaults to 0, so the unconfigured path keeps today's behavior exactly. Each setting is validated the way the rest of the file validates its environment settings and falls back to its default on a non-numeric value; a zero horizon or sentinel TTL falls back too rather than degrading into a re-fire loop. Tests drive the watcher as a real process through its public environment surface, and a separate opt-in live guard exercises a real tmux pane holding a wedged process in a disposable lab home built by bin/fm-lab-home.sh, covering both halves of the contract: the cap engaging, and the cap releasing itself on the horizon with no operator action. * no-mistakes(review): fix(fm-watch): bound the wedge cap honestly * test(fm-watch): make the wedge-cap live guard a falsifiable comparison The live guard demonstrated the ceiling but could not falsify it, and it advertised evidence that did not survive its own run. Register both new suites in bin/fm-test-run.sh. Unregistered they fell into the unclassified catch-all, which put the live guard outside the repo-wide live-gate sweep at tests/fm-live-gate.test.sh - the sweep that proves every live guard refuses together on FM_LIVE=0. The wedge-cap suite joins watcher-wake-lock and the live guard joins live-harness-optin; the serial lane passes no live-capability --fail-on-gate-skip token, so CI gating is unchanged. Run the unceiled baseline and the ceiled arm back to back on the same workload, one differing setting, in one transcript. The baseline at the shipped default of 0 shows the train of five consecutive wakes the cap exists to bound; the ceiled arm at 3 shows one terminal PERMANENTLY-WEDGED wake and then silence. State plainly in the transcript that the motivating incident happened at the default, so it motivates the ceiling and is not itself evidence the ceiling engaged. Correct the post-horizon assertion. It required the terminal marker on the very first cycle after the horizon, which the reset escalation counter makes false: the first post-horizon wake is an ordinary escalation and the bound is reached one cycle later. Assert the lane is audible, not which marker it carries. Reprint the transcript to stdout before cleanup, so the run ends in one self-contained evidence block instead of a pointer to the temp file this script is about to delete. Source tests/wake-helpers.sh for hash_text, and define PANE_TEXT in this script's own scope, so no unbound-variable or command-not-found line can appear inside the evidence a reviewer reads. Document in docs/configuration.md what the cap covers and does not: it bounds the wedge timer where a busy pane crosses FM_BUSY_TURN_MAX_SECS, it does not cover the non-terminal first-sight stale surface, only a pane leaving the stale path clears the markers, and a cap fire resets the consecutive-deferral budget of a wedge-evidence guard because that guard is not consulted while the cap is silencing the window. * no-mistakes(review): fix(fm-watch): release wedge cap markers on every episode-ending reset * no-mistakes(review): derive the live guard's window key the way the watcher does * no-mistakes(review): fix(fm-watch): probe dead endpoints under the wedge cap, scope terminal rows per episode * no-mistakes(review): fix(fm-watch): cap the dead-endpoint probe, real degraded fallback * fix(fm-watch): release the wedge cap through one episode-ending choke point The cap's release invariant - every reset that ends a wedge episode drops the cap markers with it - was wired at two inline sites and two more were missed. One of the misses is surface_nonterminal_stale, the hot path that ends the episode marker on every non-terminal stale poll, so the cap's episode tracking was broken in the common case rather than the rare one and a genuine new wedge after a pause-and-resume stayed silent for the full horizon. Enumeration had already failed once, so wiring the two missing sites would look finished and be incomplete again. Make the invariant structural instead: one choke point, wedge_episode_end, now performs every removal of the .stale-since-<key> episode marker and clears the cap markers with it, and a new assertion in the test fails on any direct rm of that marker outside it. keep-counter=1 marks a route that only restarts the escalation count while the wedge timer keeps aging. That is the same episode, not a new one, so the cap markers are deliberately not cleared there; an existing triage test pins that behaviour and it is unchanged. Both proofs are falsification-tested. The completeness assertion and the behavioural case each fail against the pre-fix watcher, and the behavioural case additionally fails when only the surface_nonterminal_stale wiring is reverted while the choke point still exists, so it pins the hot path specifically rather than passing because some other route happened to release the markers. The cap stays window-scoped with no per-hash marker, still defaults to off, and still releases on the horizon. The episode-scoped terminal row stays one per episode under the append lock, the documented first-sight containment stays, and the dead-endpoint-before-silencing work is untouched. * no-mistakes(review): fix(wm-watch): release wedge cap on every episode-end route * no-mistakes(review): fix(fm-watch): gate cap markers, probe under sentinel, release churn * no-mistakes(review): fix(fm-watch): release wedge cap on the away first sight * no-mistakes(review): fail safe the wedge escalation-count write under a configured cap * no-mistakes(test): fix wedge-cap live guard leaking its temp root * no-mistakes(review): fix(fm-watch): keep the wedge ladder reachable under a fired cap * no-mistakes(review): fix(fm-watch): pace the sentinel dead-record probe by threshold * no-mistakes(test): fix(test): give the wedge-cap suite its serial-lane weight hint so its CI shard fits the 30-minute job cap * no-mistakes(document): align wedge-cap docs with implemented release boundary * fix(docs): correct the cap markers' lifetimes in the layout reference The state/ listing claimed .wedge-permanent-<key> "only sits on disk until a later cap fire overwrites it". That is false: bin/fm-watch.sh:1214-1219 _wedge_cap_markers_clear removes .wedge-permanent-<key>, .wedge-terminal-queued-<key> and .wedge-rollback-failed-<key> outright, and it runs on every cap release, so a release takes the marker off disk rather than leaving it for a later cap fire to overwrite. The same clause also left the other two markers' lifetimes and delete semantics unstated. Each now carries its own lifetime, and the sentence records that a cap release drops all three while only the two silencing markers re-engage a window when deleted by hand. Verified against bin/fm-watch.sh: FM_CAP_HORIZON_SECS bounds the cap marker via _wedge_cap_marker_stands, FM_ROLLBACK_SENTINEL_TTL_SECS bounds the rollback-failure sentinel, and the terminal-row marker records the episode that queued the row and is dropped with the rest of the cap state. * no-mistakes(document): state the watcher's cap release boundary in its header comment * fix(fm-watch): make the wedge escalation-counter reset opt-in The upstream maintainer reviewed the wedge-cap pull request, accepted the live validation, and asked for the escalation-counter reset to become opt-in: it should only kick in when the cap is configured, so a home that has not set anything keeps today's behaviour and still gets the deep-inspection prompt for a stuck pane. The choke point wedge_episode_end gained a third state. The call sites whose pre-cap shape kept the escalation counter - the first-sight stale surfaces, where upstream dropped the wedge timer and left the count standing - now pass keep-count, so the counter survives them exactly as upstream's did while the cap is unconfigured. With a configured cap the count still restarts with the released episode, which is what lets a re-wedged window escalate from 1 again past a released marker. The other episode-ending routes are unchanged: the pre-cap watcher reset the count there too, so those keep resetting it in both postures. The new parity suite proves the property rather than asserting it: it drives one stuck-pane scenario against upstream main's own bin/ tree and against this branch, and requires the same observable outcome for the unconfigured case - the same ladder, the same demand-deep-inspection prompt, no cap marker. A configured cap in the same harness fires PERMANENTLY-WEDGED at its threshold, which is what shows the setting really reaches the watcher, so the unconfigured cases cannot pass vacuously. * no-mistakes(review): fix(fm-watch): bound the wedge cap settings to a comparable range The review step's fix agent produced this change but its own turn hit the 30-minute invocation budget, so the gate failed the step with the work uncommitted. The work is preserved in the gate's recovery ref and in the task's saved round patch; this commit carries it forward unchanged. bash's test cannot compare a decimal integer outside its own range: the comparison ERRORS rather than evaluating false, and every later comparison of such a value therefore reads as false. A value such as 99999999999999999999 passed the digit check unchanged, so the cap never engaged - the unbounded escalation train the feature exists to bound - and the \"is it zero\" test errored false as well, so the keep-count sites reset the escalation counter on a home that had configured nothing, which is the behaviour the maintainer's request forbids. The same hole let an unusable horizon make the cap marker expire at once and an unusable sentinel TTL drop the sentinel on the next poll, putting the failed-write route back on every poll. The fix bounds the accepted range at the ONE normalization choke point, where each value is read, rather than hardening every comparison site: a value bash cannot compare never reaches a comparison, because an arithmetic error is read as false at every site, which is the opposite of what each of those tests means. Values are accepted below 10^18, which always fits bash's 64-bit signed arithmetic, and the empty, non-digit, zero and negative handling keeps its documented fallbacks: 0 for the cap, 86400 for the horizon, 3600 for the sentinel. Regression cases cover a value beyond bash's integer range for each of the three settings, and the parity suite proves the unconfigured home is still observably identical to upstream main. * no-mistakes(document): docs: correct wedge-cap ranges, release routes, reset contract * no-mistakes(ci): fix the wedge-cap lint finding and pack the lane on CI measurements Upstream CI on the rework head went red on two checks, both ours. "Lint 1" failed on the new parity suite: two `return 1` statements behind `fail` were unreachable, and behind them was a real bug. `fail` lives in tests/lib.sh and exits, so inside the command substitution that reads the baseline it ended only the SUBSHELL - the outer script continued with an EMPTY baseline and reported a divergence with an empty trace instead of failing. The test's own header says it is the proof and a silent skip would be no proof at all, so the baseline now reports the specific reason and returns non-zero, and the caller's guard aborts the whole test. A baseline that cannot be materialised aborts the test rather than continuing. "Behavior portable parallel 2" failed the packing-boundary check: the wedge-cap suites were packed on provisional LOCAL measurements that overstated their CI cost, which left the serial lane without the headroom the boundary check needs to place one script exactly at the budget. The same CI run measured them for real - 374225 ms for the wedge-cap suite, 27763 ms for the parity suite, 74 ms for the live-guard skip - and those Ubuntu measurements replace the local ones in bin/fm-test-run.sh. * no-mistakes(document): docs: fix wedge-cap measurement attribution, document cap-off release
…lag (kunchenguid#7407) * feat: archive retired task status history behind an opt-in flag * fix(bin): recognize an already-retired remote home before it is recreated Sourcing fm-wake-lib.sh through the control script's libraries creates "$FM_HOME/state", so retiring a remote home that was already removed fabricated an unseeded directory before validate_home ran. The already-retired branch was unreachable, and that retirement and every retry refused with "remote home is not a seeded secondmate home". Check for the absent home before loading any library, accepting exactly the retire dispatcher's arguments so an invalid option still fails with usage, and drop the allow-absent branch that could no longer be reached. * no-mistakes(review): Removed configurable status archive retention override * no-mistakes(review): Remove published archives after failed status retirement
…y age (kunchenguid#7364) * fix(bin): raise unanswered review threads again after acknowledgement Acknowledging a maintainer signal removed the only record of the thread, so a comment nobody answered was never raised again, including while its task sat on a hold. Poll now tracks each unanswered maintainer comment, review and inline review comment on an open pull request until the contribution author replies in it or it is resolved, and raises it again once it is a day old (FM_CONTRIBUTIONS_REPLY_AGE) and daily after that. The wake names the comment URL and its age. * no-mistakes(review): Track only inline and changes-requested threads, bound notified * no-mistakes(review): Let reviewer verdicts answer reviews, skip empty-body change requests * no-mistakes(ci): Fixed "Behavior portable serial 6" with a test-only change. The serial 4 failure is a timing flake that this change did not cause. Serial 6 (tests/fm-pr-check-security.test.sh, "retired poll left task-a.check.sh"): this PR adds one more forge read to the contribution observer, a paginated `gh api graphql` query for reviewThreads. The test's fake `gh` answered every `api graphql` call with merge-state key=value lines. The observer rejected that output and raised the wake "contributions: observation unavailable ... review thread pages returned an unexpected shape". That wake ended the watcher cycle before the merged task-a poll retired. Invariant: the fake forge must answer every read the contribution observer makes, in that read's real shape. The other contribution reads (comments, reviews, inline comments, checks, statuses, repo) were already faked. The new reviewThreads query was the only read missing. Fix: tests/fm-pr-check-security.test.sh now answers any `api graphql` call containing reviewThreads with one empty reviewThreads page. The merge-state graphql answer is unchanged. Verified: I reproduced the failure locally before the fix. The test also passes on base fc8972c with that commit's bin/. After the fix, `bash tests/fm-pr-check-security.test.sh` exits 0 with all 47 cases ok. The only other test whose fake `gh` handles `api graphql` and runs the contribution path is tests/fm-pr-merge.test.sh. It prints no "unexpected shape" text. It fails locally with "github-zero-exit-queue-required: refusal did not name the concrete observed state", and it fails the same way on base fc8972c. So that failure predates this change, and it is not among the failing CI checks. Serial 4 (tests/fm-remote-reply.test.sh, "the NUL-bearing line was not captured"): this is a 40-second wait (800 polls of 0.05 seconds) for a procevent result in the remote-reply stream, and it timed out on CI. The test does not use fm-contributions or any file this PR changes. It passed locally twice (42 ok, exit 0), so this is CI load flakiness outside this change. I made no code change for it * fix(bin): make unanswered review re-raise opt-in The re-raise wake and the reviewThreads read now run only when FM_CONTRIBUTIONS_REPLY_AGE is set to a positive number of seconds. An unconfigured install polls exactly as before: no review-thread read, no threads[] entry and no unanswered signal. arm copies a set value into the generated check so the watcher honors it. * fix(bin): refuse a zero-padded zero reply age FM_CONTRIBUTIONS_REPLY_AGE=00 and 000 passed validation as positive ages. They now fail like 0 does. * no-mistakes(review): remove dead reviewThreads branch from security test fake gh * no-mistakes(review): use portable sed -i.bak in contributions test helper * no-mistakes(docs): name the thread URL in the unanswered-wake header text
… opt-in flag (kunchenguid#7631) * Refuse unguarded Herdr lifecycle briefs at spawn * no-mistakes(review): name matched Herdr command, document opt-in guard, test scaffold * no-mistakes(lint): quote matched-command assertion so ShellCheck SC2016 passes
…an opt-in flag (kunchenguid#7630) * fix(bin): publish a child's PR-ready line only after checks green behind an opt-in flag bin/fm-pr-check.sh publishes the child's PR-ready line to the parent channel when it registers the PR, before the child has reported its checks green. With config/ready-after-green present, a no-mistakes registration records the PR and arms the merge poll but publishes the line only when the last done line in the child's status is 'done: PR <url> checks green' for that PR. Without the flag the behaviour is unchanged. * no-mistakes(review): accept checks-green note with surrounding text for ready line * no-mistakes(ci): Lint 2 was caused by this PR and is fixed. Behavior tests (Herdr) is a pre-existing flake unrelated to this PR, so no code change was made for it. Lint 2 (fixed): ShellCheck SC2034 at tests/fm-teardown.test.sh:2711 because `run` was declared with `local` but is a function defined inside the test, never a variable. Invariant: a `local` declaration names only variables the function uses. The same wrong declaration also existed in the sibling opt-in test at line 2662 (both added by this PR), so `run` was removed from both `local` lines. Verified locally: `shellcheck -x tests/fm-teardown.test.sh` returns 0 (it reproduced the SC2034 warning before the edit), `bin/fm-lint.sh tests/fm-teardown.test.sh` returns 0, and `bash tests/fm-teardown.test.sh` exits 0 with 141 passing tests, including both "fm-pr-check withholds the PR-ready line ..." and "fm-pr-check releases the PR-ready line on a checks-green report that carries surrounding text". Behavior tests (Herdr) (not caused by this PR): the single failure is tests/fm-backend-herdr-presentation-e2e.test.sh "concurrent secondmate recovery failed ... error: herdr presentation recovery could not acquire its session lock; refusing a concurrent resume". That test exercises bin/fm-spawn.sh's Herdr session-lock recovery path; this PR touches only bin/fm-pr-check.sh, docs, a SKILL.md row, and tests/fm-teardown.test.sh, and the presentation test references none of them. The identical failure appeared on the unrelated branch fm/t3-5596-successor-v3 in run 39709923982 (job 119598556006), and the flake is already tracked in open issues kunchenguid#6454 and kunchenguid#7277. Rerunning the Herdr job should be enough; fixing the lock budget belongs to those issues, not this PR
* feat(bin): add Rex as an auto-detected session backend Add bin/backends/rex.sh, a session-provider adapter for the Rex terminal multiplexer, and wire it through the dispatcher, spawn, control plane, crew state, busy evidence, and away-mode supervision. - Auto-detect Rex from TERM_PROGRAM=rex with REX_BLOCK, after tmux and Herdr and before cmux; explicit --backend, FM_BACKEND, and config/backend still win, and a nested tmux or Herdr is never read as Rex. - One focus-safe tab per task: in Firstmate's own Rex session when it runs directly inside Rex (confirmed against the server), otherwise in one session per home; new blocks are resized to 200x50; every call names its session and block by immutable id. - Recovery-grade agent state from Rex's process view, OSC 7501 program status as positive busy evidence only while its owner is in the foreground chain, and a viewport derived from the last size.rows rows of the styled capture. - Absence proof from a structured not-found on the recorded server (rex_server), so fm-control exit and relaunch work on Rex and a gone block is reclaimed into a fresh tab in its recorded session. - Away mode for a Rex-hosted primary runs its daemon in a dedicated Rex session killed by exact id. - Docs: docs/rex-backend.md, configuration and control-plane tables, and a dated live verification record; tests: a stateful fake-CLI suite, a real-server smoke test guarded to its own fm-test- session, and a Rex away-launcher e2e. * no-mistakes(review): Recover Rex reclaim after server stops; document deferred asks * no-mistakes(document): Correct Rex documentation and consolidate stale backend guidance
…ollback (kunchenguid#7625) * no-mistakes(review): wedge cap: guard the cap-fire counter reset with rollback * no-mistakes(document): docs: repair shard-pack wording and wedge-cap marker shorthand * no-mistakes(review): docs: correct away-release terminal-row bound claim * no-mistakes(document): docs: cover third guarded cap-state write in watcher comment * no-mistakes(ci): Reweighted the packing hint for tests/fm-supervision-host.test.sh from the local 789123 ms to 1192918 ms, the one completed successful CI sample (run 39642852135, job 119276758966), and let the LPT packer rebalance. The guard was not weakened (PORTABLE_SERIAL_MAX_WEIGHT_MS=1200000, PORTABLE_SERIAL_SHARDS=9, coverage guard and packing-boundary check untouched) and nothing was hand-placed. New packed hint sums of the 9 serial shards (ms): 1=1192918 (1 script), 2=1139923 (18), 3=1139903 (30), 4=1139922 (30), 5=1139917 (30), 6=1139886 (29), 7=1139891 (30), 8=1139880 (29), 9=1139911 (31). Total 228 scripts = the serial lane exactly; max 1192918 < 1200000, so it fits. Packing guard: bin/fm-test-run.sh --check-coverage -> FM_TEST_COVERAGE ok total=268 parallel=24 parallel_max_ms=665545 parallel_imbalance_ms=2 parallel_unhinted=0 serial=228 serial_shards=9 serial_unhinted=15 serial_max_ms=1192918 serial_budget_ms=1200000 herdr=16, exit 0. tests/fm-test-run.test.sh passes both packing contracts: 'coverage guard bounds the unmeasured share and serial packing within twenty minutes' and 'serial packing accepts the exact budget and refuses one millisecond above it'. bin/fm-doc-audience-check.sh ok (surfaces=124 local_links=851). docs/fm-test-portable-shards.md now states the same 1192918 ms for that script, citing run 39642852135 (documented duration == packed weight), and adds the required honesty note in the same paragraph style: run 39765114423 cancelled this script alone in a shard at the 30-minute job cap and upstream run 39618934309 hangs identically, so the weight is a fit to the 20-minute packing target, not a cure, and the underlying slowness is an upstream defect this branch does not own. No other file, weight, test or doc line touched. Unrelated pre-existing failure, verified by git stash at base commit 5af4072: tests/fm-session-lock-ancestry.test.sh asserts 'the pty-host was not reparented to init after the daemon ended' and fails identically on the unmodified tree (sandbox pty behaviour); the --jobs family-admission assertion in the same suite fails only because of it. Not caused by this change
…nguid#6588) * fix(bin): restore inherited handling before main-only hand-back * Restored downtime so an earlier engine turn could not suppress a later main-owned close while leaving a healthy successor behind * Routed restoration failures through the existing Stop-hook alarm instead of silently discarding the hand-back * Added host-and-hook regressions plus attended and away idle-return controls to separate missing monitoring from status classification * no-mistakes(test): Assert ordered stale event in returned-driver controls * no-mistakes(document): Document main-only hand-back downtime restoration and failure * no-mistakes(document): Correct host header on main-only downtime republish
…unchenguid#7639) * feat(rex): push blocked edges from Rex program status through a bounded Lua reader Rex joins Herdr as a push-capable backend: a bounded rex do reader streams program_status_changed, child_exited, and block_closed for the watched task blocks, and a trusted blocked edge wakes the supervisor with Rex's blocked kind and message in the reason. The per-pane dedupe moves into the shared transition lib, the record gains a detail field, and gone edges end the wait for an immediate poll. The real-Rex test isolation is shared by both smokes. * fix(rex): anchor the composer read on Rex's cursor position Rex's format method reports the cursor alongside the styled grid, so the composer verdict now uses the same cursor-anchored read as tmux and falls back to the cursorless read only where that is unknown. OpenCode 1.19 draws a status line below its composer, which made every cursorless read of an idle OpenCode worker on Rex unknown and refused steers, exit, and relaunch. * test(rex): add a live composer guard for every installed harness on Rex * docs(rex): record push-event and per-tool live evidence on Rex * no-mistakes(document): Clarify Rex documentation and shared transition ownership * no-mistakes(ci): Fixed flaky public-followup fixture teardown: killing only the remote worker child allowed its supervisor to restart and race directory removal. Cleanup now uses the existing whole-worker-tree stop helper. Verified the full public-followup suite, remote-job orphan-reap suite, shell syntax, and git diff checks all pass
…nd an opt-in flag (kunchenguid#7634) * refactor(bin): share the branch-to-PR lookup through fm-pr-lib * feat(bin): notify owned PR health changes behind an opt-in flag * no-mistakes(review): drop status-line ownership, key checks per name, remove PR wait rule * no-mistakes(review): rediscover the branch PR once the cached one is final * no-mistakes(review): refuse retired re-enrollment, require marked checks, verify owned records * no-mistakes(document): note shared branch-to-PR lookup in scripts table * fix(bin): notify reported failures when no checks are required * no-mistakes(document): inventory PR health flag and state, note new pr-forge member
* Isolate real Rex tests with private servers * no-mistakes(review): Trap Rex cleanup early and prove passive-read autostart guard * no-mistakes(document): Consolidate Rex test isolation documentation
* fix(bin): report an outdated bootstrap tool instead of a missing one detect_local_tools printed "MISSING:" for a treehouse without --lease, a no-mistakes below 1.46.0, a gh-axi below its floor, and an incompatible tasks-axi or quota-axi, so a supervisor read an installed tool as absent and took the first-install remediation again. Report those as "OUTDATED:" with the installed version, the requirement and the tool's own upgrade command: treehouse update, no-mistakes update, gh-axi update and quota-axi update. tasks-axi ships no self-updater, so it keeps its install command. "MISSING:" stays for absent tools only, and lavish-axi keeps PRESENTATION_UNAVAILABLE with its text fallback. Update the bootstrap-diagnostics handler, the AGENTS.md section 13 trigger list and the operator diagnostics in docs/configuration.md, and cover absent, below-floor and at-floor for every floored tool in tests/fm-bootstrap.test.sh. * no-mistakes(document): Documented distinct bootstrap tool diagnostics * fix(bin): reuse the tasks-axi check's own probe results Rendering the tasks-axi OUTDATED requirement text ran the two `--help` capability probes again after the compatibility check had already run them, including when session start handed bootstrap an incompatible verdict. The check now records the reason behind its verdict - the version floor, or the capability probes the build fails - and reuses it, both within the process and across the existing one-hop handoff, so naming a missing capability costs no second probe pass. A build below the floor is answered by its version alone and never pays for capability probes it cannot use. The bootstrap tests that simulate a tool as absent removed it from the fake bin directory while bootstrap kept searching the fallback base PATH, so a real host install could satisfy the lookup instead of the expected MISSING line. Those cases now hide the tool from the base path as well, and a new case pins the contrast between a bare and a curated fallback. Verified: the touched bootstrap suite passes, and its probe-count and capability assertions fail against the pre-fix scripts. Run with a fallback base PATH carrying real installs of the absent tools, the pre-fix suite fails on "absent treehouse reports a missing tool" while the fixed suite passes every case. * no-mistakes(document): Documented missing versus outdated tool diagnostics * no-mistakes(document): Refresh outdated-tool documentation * no-mistakes(review): Reuse shared version parser for outdated diagnostics * no-mistakes(document): Correct outdated-tool diagnostics documentation * no-mistakes(document): Align toolchain upgrade documentation * fix(skills): keep main's bootstrap-diagnostics description and only add OUTDATED
…e verbatim match (kunchenguid#7530) * Clarify ahoy legacy startup exclusion * Fix quoted payload code span
…unchenguid#7637) * fix(timeout): bypass shell builtins when invoking external runners * no-mistakes(review): Remove redundant slashless runner resolution from external timeout
…urable (kunchenguid#6094) * fix: count answered captain holds in the Done archive at the completion gate Done retention moves closed markdown rows into the configured Done archive, which tasks-axi show never reads, so verify, complete, and scout teardown refused an answered captain hold once retention archived it. The completion gate now reads the newest archived block for an attested entry, under its exact or legacy identity, and accepts it only through a recorded resolution. Unanswered archived closes and holds found nowhere still refuse. * no-mistakes(document): document Done-archive durability in captain-hold verification record * no-mistakes(ci): Fixed the selected Greptile findings in bin/fm-captain-hold.sh (all in the Done-archive read path added by this PR). code_change_needed=true. ci-3 (Archived reads can hang): task_show_archived() now bounds the replayed archive `tasks-axi show` with the same read bound the live backlog read uses (FM_BACKLOG_ROW_TIMEOUT_SECS via fm_run_timed, sanitising a non-positive/padded value to 10s, mirroring fm_backlog_row_show). A bound hit is treated as 124, not absence: task_show_archived returns 124 and prints a loud reason; archived_entry() propagates 124; verify_entry_durable() exits 124 on the archived-read bound (alongside its existing resolve 124 handling); verify_hold_durable() exits 124 rather than reading the row as absent. So verify can no longer hang and complete can no longer stay blocked while holding the origin metadata lock. ci-4 (User-level archive setting ignored): added captain_markdown_archive_setting(), which resolves the [markdown] archive key with tasks-axi's own source precedence — the backlog root's .tasks.toml first, then $HOME/.tasks-axi/config.toml — before the existing default of done-archive.md beside the backlog; a relative value still resolves against the backlog root, which I verified empirically matches tasks-axi (root config wins over home config; a relative home-config value resolves against the backlog root; default stays data/done-archive.md). tasks-axi exposes no command for the archive path, so re-parsing with matched precedence is the available approach. Added test_archived_answer_in_a_user_configured_archive_satisfies_the_gate, which sets the archive only in a fixture $HOME/.tasks-axi/config.toml and drives the whole hold→answer→retention→verify/complete/teardown flow with that HOME, asserting the answered call archived to the relocated location still satisfies the gate. ci-5 (repeated archive scans, P2): left unchanged per the instruction's opt-out. A clean reuse of the row archived_entry already found is not small: archived_entry runs inside a command substitution, so its TASK_SHOW_OUTPUT dies with the subshell and cannot be handed to verify_hold_durable without restructuring both to pass the loaded row through the current shell. The growth is bounded (O(archive size) per attested entry) and P2, so hardening it would add more machinery than the fix warrants. Unanswered archived closes and holds found nowhere still refuse (existing test coverage retained). Verification: `bash -n` clean; pinned ShellCheck 0.11.0 (`shellcheck -x bin/fm-captain-hold.sh`) reports 0 findings; tasks-axi archive-precedence behaviour confirmed by direct experiment. The full fm-captain-hold-lifecycle.test.sh suite (which exercises both the pre-existing archived-gate test and the new user-config test) is slow due to beads spawns and was still running at report time; recommend confirming it green before merge. ci-1/ci-2 need no code change (action_required fork workflow runs awaiting maintainer approval), per the recorded user decision * no-mistakes(document): Document Done-archive reads under backlog row timeout * docs: say only the archive row replay is bounded, not the archive scan * no-mistakes(document): Move Done-archive rule to the complete section
…kunchenguid#7627) * fix(bin): optionally read a PR with no CI checks as unverified, not checks green nm_ci_checks_state maps the ci log marker "no CI checks reported - still monitoring" to green, the same verdict as "all CI checks passed". A pull request that no check ever ran against therefore surfaced as "checks green: PR ready for review" (kunchenguid#3216). Add a no-checks verdict for that marker, returned only in a home that creates config/ci-no-checks-not-green. The run-step consumer reports working with a detail naming the real condition, and the status-log override no longer promotes a crew's own "checks green" report while that marker is the latest. Without the flag every reading is unchanged. Co-authored-by: Harlan Do <hieudtr8@gmail.com> * no-mistakes(document): document ended-monitor reading under the zero-checks flag * fix(bin): read a PR with no CI checks as unverified on every home The ci log marker "no CI checks reported - still monitoring" now always yields the no-checks verdict, so a pull request that no check ran against is reported as working instead of checks green (kunchenguid#3216). The opt-in flag, its docs section, and its skill entry are removed, the default-path test now expects working, and the usage header and architecture note describe the no-checks reading. --------- Co-authored-by: Harlan Do <hieudtr8@gmail.com>
… test root-safe (kunchenguid#7650) * no-mistakes(review): correct the counter-reset log line, the sentinel comment, and the root proof Three findings the review step recorded but never applied, all in this branch's own change. The clean-rollback branch of the counter-reset guard logged "cap state rolled back", copied from the sibling marker-write branch. That is wrong here: the cap marker was already written before this block and _wedge_cap_rollback only truncates the counter, re-arms the since-file and clears write tracking - it never clears the markers. The window therefore stays silenced for FM_CAP_HORIZON_SECS, so the one operator-facing line must say that rather than claim the cap state was rolled back. The rollback behaviour is unchanged and no marker-clear call is added: the marker is deliberately kept so the window stays quiet. The sentinel short-circuit comment opened "from either route that has to defend cap state" and then enumerated three routes. Wording only. The new regression case injected its fault with chmod 444, which does not restrict root, so under root the guard would never fire and the case would fail on its own exit-code assertion. It now skips as root the way this suite's other mode-based faults do. The assertions are untouched: as the ordinary user the case still proves the guard. * no-mistakes(review): name both ways a capped window leaves the silence in the reset log line The tier-0 message for a refused post-fire escalation-counter reset says the cap marker stands, so the window stays silent until its horizon elapses. That names one release route where there are two: wedge_episode_end drops the marker through _wedge_cap_markers_clear, so any episode-ending route ends the silence early, and docs/configuration.md records deleting the marker as the operator's way to re-engage a capped window at once. This tier writes no sentinel, so the marker is the only thing left to remove. The message now names the horizon, an episode-ending route, and the operator remedy, in one line in the same shape and position as its sibling tier-0 messages. The rollback behaviour, the marker handling and the tier are unchanged. * no-mistakes(document): correct root-skip comment's mode-bit precedent in wedge-cap test
* fix(bin): resurface wakes appended while a handling successor is parked The supervision host parks on the handling successor it starts for a wake its engine handled, and a handling successor never resurfaced recovery, so a row another script appended during the park (a captain inbox note from the Basecamp integration) sat queued until an unrelated close - four hours in the observed case. A handling successor now records the newest queued sequence at its start and closes once with the recovery wake while a newer row is still queued; a newer row already consumed only advances that mark, so the predecessor's delivered episode is still never re-announced. Follow-ups, not built here: - A row appended in the short window between a handled close and its successor's start (about a second) is below the successor's mark; if the engine handles that close without claiming the row, the row waits for the next close. Closing that window needs the host to treat rows left queued after a handled attended turn as main's, which is a separate host change. - After a machine reboot the primary session does not restart on its own, so queued wakes wait until it is resumed by hand. Either auto-resume the primary session after boot, or alert the owner when the primary session is down while wakes are queued. * no-mistakes(review): Close handling-successor startup wake race * no-mistakes(test): Stabilize parked-host inbox wake regression timeout * no-mistakes(document): Clarify handling-successor wake continuity guarantees * no-mistakes(ci): Stabilized the flaky remote secondmate lifecycle test by extending the auto-relaunch watcher deadline from ~30 to ~60 seconds. The CI runner exceeded the old deadline by about 3 seconds; the changed production branch was not reachable in this fixture. `bash -n` and `git diff --check` pass. The full test was attempted but exceeded the 600-second local execution limit before reaching this case * no-mistakes(ci): Fixed flaky concurrent Herdr recovery testing by retrying only documented session-lock contention after both competitors finish. Cleanup now handles fixture-owned read-only hooks. Targeted E2E passed; bash syntax and diff checks passed. Shellcheck reported only existing SC1091 source-following notices * no-mistakes(test): Preserve successor wake boundaries across handoffs * no-mistakes(test): Revert successor boundaries and drain teardown fixture wake * no-mistakes(test): Notify main when downtime handback publication fails * no-mistakes(test): Revert downtime handback notification changes * no-mistakes(document): Correct stale supervision wake routing documentation * fix(bin): stop the supervision host deadlocking and re-announcing handed-back closes Two defects in this branch's handling-successor changes surfaced as the "Behavior portable serial 2" shard hanging at its 30-minute limit. A TERM that lands while the host settles a handled turn's successor handoff left the host holding the wake-queue and recovery-marker locks; its exit cleanup then waited forever on the queue lock it still owned. The cleanup now releases those locks if this process owns them before releasing its grant. When an accepted close turns main-only at its turn, the host hands it back and leaves the successor running, but republished the episode as pending downtime, so that successor re-announced the same close and exited. The handback now republishes the episode as already announced; a later durable append still opens a pending episode the successor recovers. * no-mistakes(test): Park on successor after handling settlement failure * no-mistakes(test): Use tab-delimited handled-settlement failure logs --------- Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com>
* fix(bin): keep accented letters in pending-reply request summaries fm_pending_reply_summarize deleted every byte outside printable ASCII, so a Portuguese summary lost its accents: "não faça a correção" was stored as "no faa a correo". It now removes control characters only. The 120-character cap counts UTF-8 characters from lead bytes, so it never splits a letter under any caller locale, including the C locale. * no-mistakes(document): Documentar preservação de acentos nos resumos --------- Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com>
…id#7149) * Add explicit OpenRouter Jev dispatch resolution * no-mistakes(document): Clarify dispatch documentation and remove stale duplicates * chore: exclude local Stele binding from PR * no-mistakes(document): Correct dispatch help and never-send documentation * ci: retrigger CI after a timed-out test shard * no-mistakes(document): Correct dispatch architecture documentation ownership
…imits (kunchenguid#6983) Opt-in: fm-watch.sh publishes state/lane-capacity.json via bin/fm-capacity.sh --publish only while config/lane-capacity exists; adds beat pressure and keyed limits to fm-capacity.sh. Unconfigured fm-capacity output unchanged.
…#5359) * fix(bin): transport contribution-input JSON through slurpfile, not argjson bin/fm-fleet-snapshot.sh --contribution-input passed the full backlog and task-record JSON to jq through --argjson. A single --argjson argument is capped by the kernel's MAX_ARG_STRLEN (131072 bytes on Linux), so a large enough backlog made the kernel refuse to exec jq while the script still exited 0 with a broken result. Transport both documents through temporary files and --slurpfile, matching the pattern the default snapshot path already uses, and propagate a non-zero exit when that jq invocation fails. Fixes kunchenguid#5349 * test: narrow contribution-input regression to final assembly failure The ARG_MAX transport is already fixed on main by kunchenguid#6830, so keep only the regression proving a failing final jq assembly exits non-zero.
* Fix Rex secondmate session lifecycle * no-mistakes(review): Retire Rex secondmate sessions under the owning home identity * no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches * no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation
…gins, and exit (kunchenguid#7336) * feat(opencode): add OpenCode 2 support with version-gated launch and plugins OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.* plugins now export a default definition with an id and a setup function alongside their legacy OpenCode 1 server export, backed by a new shared lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check, pretool-check, and sessionstart-nudge install their behavior from setup(ctx) on OpenCode 2 and keep the server entrypoint for OpenCode 1. fm-spawn probes the resolved opencode executable's --version once and branches the launch, model flag, reasoning config, exit key, and busy-state plugin off the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model, the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the session.execution.* busy plugin); OpenCode 1.x and any undetected or unparseable version fall back to the 1.x shape (--model, 1.x config, /exit) everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1 keeps the pre-port session.status/session.idle plugin so its busy-state wiring is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter and supervision docs, OpenCode 2 live fixtures, and regression tests for the unknown->1.x launch path and the version-gated busy-plugin shape are included. * no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd * feat(opencode): launch detected OpenCode 2 workers with --standalone OpenCode 2's default launch attaches to the shared background service, which hosts plugins with the service's environment and ignores the pane's OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions, FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never found its root or delivered a prompt. Interrupted and failed turns now end a turn like a succeeded one, while a server shutdown does not. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e A private opencode serve plus a loopback provider proves the five primary plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection guards, turn-end guard, watcher arming across succeeded, interrupted, and failed turns. The fixture declares the complete model capabilities object, which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs it across several OpenCode 2 executables. The credentialed half now picks its run flags and session tables by detected major, so it keeps working on 1.x. Fixture ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it. After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts. * docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate. * no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit * no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence * no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation * no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs * test(opencode): verify OpenCode 1.x live with the dual-export plugins Pin the live e2e TUI panes to the resolved opencode executable, and record OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the turn-end guard, watcher continuity, and 1.x worker launch and exit. * no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs * no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes --------- Co-authored-by: Niranjan <nniranc@gmail.com> Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t the most recent group member (kunchenguid#7662) * fix(bin): resolve the tmux container from the caller's own session, not the most recent group member * no-mistakes(test): Fall back when the $TMUX session id is stale * no-mistakes(document): Document tmux container choice under grouped sessions
…er (kunchenguid#7647) * fix(bin): read a fully dim Cursor placeholder as an empty composer cursor-agent 2026.10.19 draws the composer glyph and its idle placeholder as one SGR 2 run with no reverse-video cell, so ghost stripping empties the row and the empty-content guard returned unknown. Relaunch and exit then refused on every idle Cursor pane on tmux. When a styled bare row strips to empty and the plain row is an agent glyph followed by an anchored idle placeholder, read it empty. Bright typed text, other dim text, and unstyled rows keep their existing verdicts. Fixes kunchenguid#7413 * docs(verification): record the fully dim Cursor placeholder verdict and refresh command
… composer as empty (kunchenguid#6163) * fix(bin): recognize a named Claude session's titled composer rule on cursorless backends `claude -n <name>` right-aligns the session name in the rule above the composer. Cursorless backends (Herdr) accepted only solid rules, so the scan saw the lower rule alone, the idle composer read `unknown`, and away-mode delivery to a named Claude primary deferred until return. A rule carrying one right-aligned title now opens a separator pair. It never closes one, the pair it opens counts only over an agent-glyph row, and it is never a valid Pi pair, so Pi's verdict is unchanged. The away-mode composer deferral now names which refusal it was: unsubmitted text, or no composer shape recognized. * no-mistakes(review): Reword composer deferral reason to cover busy harness * no-mistakes(ci): ci-3 (Greptile, user chose to fix): The rule that must always hold is that Claude's titled opening rule is accepted for any non-empty session name that doesn't start with a space and doesn't contain the rule glyph `─`, including a multiword name. The only place that check lives is the title `case` in `_fm_composer_titled_rule_row` (bin/fm-composer-lib.sh). It now reads `case "$title" in ''|' '*|*─*) return 1 ;; esac`, as the user asked, and the comment above the function mentions multiword titles. I added regression fixtures to `test_matrix_claude_named_session_titled_rule` in tests/fm-composer-lib.test.sh, next to the existing single-word `Andrew` cases. They use the title `Andrew Chen` and check that an idle composer on herdr reads `empty` and a composer with typed text reads `pending`. Something you should know: the old pattern already accepted a title with a space in the middle. It only rejected a title ending in a space. So the new multiword fixtures also pass against the old code, and the bot's failure scenario did not actually happen for "Andrew Chen". What the fix really changes is that it drops the trailing-space rejection, as instructed. Checks run: tests/fm-composer-lib.test.sh and tests/fm-composer-ghost.test.sh both pass (exit 0, no failures), and `shellcheck -x` is clean on both changed files * no-mistakes(review): Match unnamed-ghost fixture rule width to the partner rule
…process groups (kunchenguid#7666) * fix(tests): reap Pi test process groups and stop arms whose ground is removed Orphaned stub-arm pollers from tests/fm-pi-watch-extension.test.sh outlived a failed case: they waited for release/stop files the failed driver never wrote, teardown only removed the temp root, and the loops had no other exit. Run each node driver in its own recorded process group and reap those groups in teardown before the root is removed; end every blocking stub loop once its own script is gone. bin/fm-watch-arm.sh now exits with a typed ground-removed line within one poll when its script, state directory, or home is deleted, instead of polling through the successor window. * no-mistakes(document): Document arm exit when its ground is removed
…alidating (kunchenguid#5947) * fix(bin): defer wedge escalation while an attributed validation run is running or fixing While a no-mistakes run executes a round, the worker is correctly idle behind its own blocking drive call, yet the watcher's wedge timer escalated a possible wedge every FM_STALE_ESCALATE_SECS. The timer now consults the attributed run step before escalating: a run the current-state reader reports as validating (running) or validating (fixing) for a named run defers onto the FM_PAUSE_RESURFACE_SECS recheck cadence, aged from a marker bound to that run id. Parked, terminal, ci-monitoring, coarse, runless, absent and unreadable runs keep the unchanged escalation ladder. * test: count only the validating-run read in the unarmed parked-gate probe * no-mistakes(document): add .validating-* to watcher-internals marker inventory * no-mistakes(document): Update fm-watch.sh comments for validating-deferral chain * no-mistakes(document): Document validating-run deferral in guard and handoff docs * feat(watch): gate the validating-run wedge deferral behind config/wedge-defer-validating The deferral changed the default for every home, unlike the comparable config/wedge-defer-parked-gate and config/wedge-defer-worker-wait waits. Absent the new presence flag the wedge timer spends no current-state read for it and keeps the unchanged ladder.
…unchenguid#6300) * fix(session-lock): identify a harness session on Windows/Git Bash Every session start on Cygwin (Git for Windows) refused the fleet lock and dropped to read-only with "cannot locate harness process in ancestry", so spawning, steering, merging, the wake-queue drain, and supervision repair were skipped on every start. Two independent causes, both on the identity path. Cygwin's ps has no -o option at all and fails the whole invocation with "unknown option -- o", so the ancestry walk aborted on its first hop. The walk now reads comm, args, and ppid through accessors that fall back to Cygwin's fixed ps columns, leaving the procps/BSD path unchanged. That alone does not resolve the session: the parent link from a shell the harness spawns does not cross the Cygwin boundary, and Cygwin reports that shell's PPID as 1, so no walk can reach a harness that is a native Windows process. Identity is instead taken from the session pid the harness publishes and confirmed against the Windows process table before it is used - the pid must still be live and its executable must independently identify a verified harness - so an absent, stale, or non-harness value is discarded rather than bound. Walking the real Windows parent chain was implemented and then removed as unsafe. MSYS emulates exec by spawning a fresh Windows process and exiting the old one, so intermediate shells vanish and a child's recorded parent is routinely a pid that no longer exists; Windows never reparents an orphan, so that dangling id stays and can be reissued to an unrelated process. Following it can bind a home's lock to the wrong process, which is the failure this file exists to prevent. A harness that publishes nothing stays unresolved, which leaves the session read-only exactly as before. Windows pids are tagged rather than stored bare. They are a different namespace: kill -0 reports a live Windows process as dead, and the number can collide with an unrelated live Cygwin pid. The tag makes the value non-numeric, so a consumer that treats it as a local pid - including a future kill - refuses it instead of acting on the wrong process. fm-sessionstart-nudge.sh carried a private second copy of the ownership walk and so stayed wrong after the owner was fixed, nudging a session that already held the lock. It now asks the owning function. Verified on Windows 11 (Git Bash, Cygwin ps 3.4.10): the lock is acquired, reports its holder, is idempotent, and refuses a bogus, dead, or non-harness published pid. Regressions cover both platform departures behind a fake process table, so they run on Linux and macOS CI too. The pre-existing e2e failure in this suite on Windows is unchanged from main; it cannot exec its symlinked fixture. Refs kunchenguid#3396 Claude-Session: https://claude.ai/code/session_01F9T29YDqYSkQeDTC2Nfi7h (cherry picked from commit 8b281b7) * fix(session-lock): keep the nudge's own ancestry question intact Delegating the nudge to fm_session_lock_owned_by_self changed the question it asks. The nudge asks whether a process in this ancestry took the lock; the ownership predicate additionally requires a verified harness in that ancestry, so a session whose lock was written by a plain shell started being nudged to run session start again. tests/fm-sessionstart-nudge.test.sh pins the looser contract deliberately. The walk stays local, now reading ppid through the portable accessor so it also survives a ps with no -o option, and defers to the ownership predicate only for a Windows-tagged holder, which is not in this process table at all. (cherry picked from commit 0baf64f) * fix(session-lock): accept a tagged identity in every gate that reads the lock The Windows identity added in this branch introduced a second shape into state/.lock. Six gates read that field, and each one answered "is this value usable" with its own inline numeric test, so every one of them read a valid Windows holder as malformed. The visible cost was concentrated in startup completion: the record was never written, and the clear/compact check that consumes it could never match, so every clear or compact on Windows repeated the full startup sequence. The deferred network sweeps reported ownership as changed when it had not, and the Stop auto-arm treated a dead Windows session as an unreadable lock rather than a recoverable one. fm-lease.sh was the outlier: rather than refusing a value it could not use, `tr -cd '0-9'` reduced the tag to its digits and produced a number naming an unrelated process in the local table. It happened to fail closed downstream, but deriving a wrong-namespace pid is precisely what the tag exists to prevent, so it now takes the value whole and requires a local pid. fm_session_pid_valid is now the single owner of that question and every gate delegates to it, so a third identity shape cannot split them again. Verified against the shipped bytes of each gate with a tagged lock: startup completion now records and is recognized (main's gate reruns the full startup on the same input), both network-ownership gates authorize their sweeps, and a tagged lock yields no lease holder pid instead of 7204. (cherry picked from commit 9256182) * fix(session-lock): gate the tagged identity on Windows and read it in every lock reader Accept win:<digits> only where fm_win_boundary_applies, with an all-digit remainder, so a Linux/macOS reader of a win:N lock stays inert as before. The sessionstart nudge routes its tagged case through fm_win_untag_pid. fm_session_lock_foreign_owner_live and fm_session_lock_inspect now validate through fm_session_pid_valid and resolve a tagged holder through fm_harness_pid_alive, so a live win:N holder reads as a live foreign owner and as held; unverifiable tagged holders stay unknown. * no-mistakes(document): Document Windows tagged session-lock identity in nudge and scripts docs * fix(session-lock): keep task leases and stale detection working for a tagged holder A Windows session can now hold the session lock as win:<pid>, but the task lease still accepted only a local pid: the claim recorded the short-lived fm-lease.sh process instead, and fm_lease_live rejected the tagged lock, so the lease was never live and the other supervision actor could clear it and act on the same task. The lease now records a live tagged lock holder whole and treats it as live while it is still the lock holder and a verified harness in the Windows process table. The session-lock library is loaded only for a win:-shaped value, so POSIX lease verdicts are unchanged. fm_session_lock_inspect left an exited tagged holder unknown, because fm_win_command could not tell an absent pid from an unreadable table. It now returns 1 only when a readable table proves the pid absent and 2 when the table cannot be read, so inspection matches the local-pid contract: absent is stale, a live non-harness is unknown with no live harness, and an unreadable table stays unknown. * no-mistakes(review): Restrict caller-supplied lease holder pid to local numeric * no-mistakes(review): Pass tagged session-lock holder whole to task lease * no-mistakes(review): Make tagged lease-holder regression tests fail without their fixes * fix(session-lock): take a Windows CLAUDE_PID only with its session id On Windows the session identity comes from CLAUDE_PID, which every process started from a Claude pane inherits, so an unrelated process carrying a copy could take the lock as that session and both would own it. The bridge now accepts CLAUDE_PID only together with CLAUDE_CODE_SESSION_ID. The trusted session-id gate matches the tagged win:<pid> form, so that id is recorded beside the lock, and the anchor stays tagged. fm-lock.sh and the ownership readers refuse the same tagged pid under a different recorded id as another live session's. A non-Claude primary hand-started from a Claude pane inherits both values and still resolves as that session; that remains a known limitation. POSIX behavior is unchanged: every new branch applies only to a tagged pid. * fix(session-lock): let a Windows /clear re-key its own lock /clear keeps the Claude process, so CLAUDE_PID stays while the session id changes. Refusing the same tagged pid under a different recorded id locked a session out of its own lock after /clear, which POSIX handles by re-keying the sidecar. Drop that refusal: a Windows CLAUDE_PID is still taken only together with its CLAUDE_CODE_SESSION_ID, and that id is still recorded beside the lock and re-keyed on /clear as on POSIX. * no-mistakes(review): Give tagged host test a session id; clarify Windows lock error * no-mistakes(document): Point the trust-gate comment to the Windows limitation --------- Co-authored-by: Lloyd <lloydkakunguwo@gmail.com>
…d#7421) * feat(bin): add opt-in local Claude usage threshold warner A usage window is exhausted without warning: work is cut off mid-run rather than stopping deliberately, and nothing says a window is close to its cap until it is already gone. bin/fm-usage-warner.sh is a threshold warner, not a usage viewer. quota-axi is already the one Claude-quota reader this repo depends on (bin/fm-quota-axi-lib.sh), so the warner runs that existing read rather than adding a second reader that could drift from it: one bounded `quota-axi --provider claude --json --full --no-credential-refresh`, the snapshot validated with fm_quota_json_valid, and the provider row picked with the same FM_QUOTA_ROW_JQ join the dispatch consumers use, under schema 5 and schema 6 alike. Thresholds are `<window-id>:<percent>` directives in config/usage-warner, naming quota-axi's own window ids (five_hour, seven_day, model:<name>) rather than a second vocabulary, and compared against each window's own percentUsed. Naming the window is the point: the session and weekly windows exhaust at very different times, so a figure that does not say which window is close misleads. One read with two callers and no new schedule: `check` is what a human runs on demand and what the watcher runs on its ordinary check-shim cadence once `arm` registers state/usage-warner.check.sh through bin/fm-check-register.sh. There is no daemon. Warnings post through macOS Notification Center with the same osascript call bin/fm-supervise-daemon.sh's wedge alarm makes, without sourcing or coupling to that daemon's away-mode-specific config and rate limiting. Delivery is edge-triggered against state/.usage-warner: a crossing warns once, stays quiet while it holds, and re-arms when the window next reads back below its threshold, the same standing-report contract fm-mail-check.sh and fm-tool-update-check.sh already use. bin/fm-procevent-quota.sh already reads the same snapshot, so docs name the boundary rather than leaving two similar surfaces unexplained. That source fires at most once, wakes firstmate with a durable check event, and compares a provider's aggregate effectivePercentRemaining. This warner is the standing, human-facing half: per-window, delivered to the person through Notification Center, and self-re-arming. Neither replaces the other and arming both is reasonable. Opt-in is load-bearing and enforced in code. With no valid directive, `check` returns before any read and `arm` refuses, so an unconfigured home gains no behaviour change, no new failure mode, and no new hard dependency at dispatch. Notification Center is macOS-only, so `arm` refuses on other platforms instead of registering a check that could never speak. The warner only warns. It does not stop or pace dispatch, and its coverage is exactly what quota-axi can see locally, so a plan or organisation spend cap quota-axi does not report is not covered; the doc says so rather than implying coverage it does not have. The new test script carries no measured CI duration hint yet, so the portable-serial lane balances it on the conservative default until the hints are next refreshed from a green run, per docs/fm-test-portable-shards.md. * no-mistakes(review): Key usage-warner de-dupe on window and threshold; fix docs * no-mistakes(review): Keep absent-window crossings recorded; stub osascript in tests * no-mistakes(document): Document absent-window de-dupe carry-forward in usage-warner header * fix: correct Rex secondmate grouping and lifecycle (kunchenguid#7656) * Fix Rex secondmate session lifecycle * no-mistakes(review): Retire Rex secondmate sessions under the owning home identity * no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches * no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation * feat(opencode): add OpenCode 2 support with version-gated launch, plugins, and exit (kunchenguid#7336) * feat(opencode): add OpenCode 2 support with version-gated launch and plugins OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.* plugins now export a default definition with an id and a setup function alongside their legacy OpenCode 1 server export, backed by a new shared lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check, pretool-check, and sessionstart-nudge install their behavior from setup(ctx) on OpenCode 2 and keep the server entrypoint for OpenCode 1. fm-spawn probes the resolved opencode executable's --version once and branches the launch, model flag, reasoning config, exit key, and busy-state plugin off the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model, the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the session.execution.* busy plugin); OpenCode 1.x and any undetected or unparseable version fall back to the 1.x shape (--model, 1.x config, /exit) everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1 keeps the pre-port session.status/session.idle plugin so its busy-state wiring is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter and supervision docs, OpenCode 2 live fixtures, and regression tests for the unknown->1.x launch path and the version-gated busy-plugin shape are included. * no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd * feat(opencode): launch detected OpenCode 2 workers with --standalone OpenCode 2's default launch attaches to the shared background service, which hosts plugins with the service's environment and ignores the pane's OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions, FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never found its root or delivered a prompt. Interrupted and failed turns now end a turn like a succeeded one, while a server shutdown does not. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e A private opencode serve plus a loopback provider proves the five primary plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection guards, turn-end guard, watcher arming across succeeded, interrupted, and failed turns. The fixture declares the complete model capabilities object, which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs it across several OpenCode 2 executables. The credentialed half now picks its run flags and session tables by detected major, so it keeps working on 1.x. Fixture ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it. After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts. * docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate. * no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit * no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence * no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation * no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs * test(opencode): verify OpenCode 1.x live with the dual-export plugins Pin the live e2e TUI panes to the resolved opencode executable, and record OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the turn-end guard, watcher continuity, and 1.x worker launch and exit. * no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs * no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes --------- Co-authored-by: Niranjan <nniranc@gmail.com> Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(bin): add opt-in local Claude usage threshold warner A usage window is exhausted without warning: work is cut off mid-run rather than stopping deliberately, and nothing says a window is close to its cap until it is already gone. bin/fm-usage-warner.sh is a threshold warner, not a usage viewer. quota-axi is already the one Claude-quota reader this repo depends on (bin/fm-quota-axi-lib.sh), so the warner runs that existing read rather than adding a second reader that could drift from it: one bounded `quota-axi --provider claude --json --full --no-credential-refresh`, the snapshot validated with fm_quota_json_valid, and the provider row picked with the same FM_QUOTA_ROW_JQ join the dispatch consumers use, under schema 5 and schema 6 alike. Thresholds are `<window-id>:<percent>` directives in config/usage-warner, naming quota-axi's own window ids (five_hour, seven_day, model:<name>) rather than a second vocabulary, and compared against each window's own percentUsed. Naming the window is the point: the session and weekly windows exhaust at very different times, so a figure that does not say which window is close misleads. One read with two callers and no new schedule: `check` is what a human runs on demand and what the watcher runs on its ordinary check-shim cadence once `arm` registers state/usage-warner.check.sh through bin/fm-check-register.sh. There is no daemon. Warnings post through macOS Notification Center with the same osascript call bin/fm-supervise-daemon.sh's wedge alarm makes, without sourcing or coupling to that daemon's away-mode-specific config and rate limiting. Delivery is edge-triggered against state/.usage-warner: a crossing warns once, stays quiet while it holds, and re-arms when the window next reads back below its threshold, the same standing-report contract fm-mail-check.sh and fm-tool-update-check.sh already use. bin/fm-procevent-quota.sh already reads the same snapshot, so docs name the boundary rather than leaving two similar surfaces unexplained. That source fires at most once, wakes firstmate with a durable check event, and compares a provider's aggregate effectivePercentRemaining. This warner is the standing, human-facing half: per-window, delivered to the person through Notification Center, and self-re-arming. Neither replaces the other and arming both is reasonable. Opt-in is load-bearing and enforced in code. With no valid directive, `check` returns before any read and `arm` refuses, so an unconfigured home gains no behaviour change, no new failure mode, and no new hard dependency at dispatch. Notification Center is macOS-only, so `arm` refuses on other platforms instead of registering a check that could never speak. The warner only warns. It does not stop or pace dispatch, and its coverage is exactly what quota-axi can see locally, so a plan or organisation spend cap quota-axi does not report is not covered; the doc says so rather than implying coverage it does not have. The new test script carries no measured CI duration hint yet, so the portable-serial lane balances it on the conservative default until the hints are next refreshed from a green run, per docs/fm-test-portable-shards.md. * no-mistakes(review): Key usage-warner de-dupe on window and threshold; fix docs * no-mistakes(review): Keep absent-window crossings recorded; stub osascript in tests * no-mistakes(document): Document absent-window de-dupe carry-forward in usage-warner header * no-mistakes(review): Serialize usage-warner check, dedupe directives, drop vacuous greps * no-mistakes(document): List usage-warner check lock in state layout --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Kostadin Pop-Kochev <kostadin.popkocev@gmail.com> Co-authored-by: Niranjan <nniranc@gmail.com> Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(bin): use clock-stable /proc starttime for remote job worker ownership Linux remote job ownership compared ps lstart, which drifts on WSL2 while /proc starttime stays fixed, so ensure treated a healthy worker as dead and started another supervisor on every call. fm_remote_job_process_start now emits a proc-starttime token when /proc is readable and falls back to ps lstart otherwise. The Linux start path hands a live worker of the configured root whose lock still records a legacy start over to one replacement, checked against the recorded and live command so no unrelated pid is signaled. Fixes kunchenguid#2921 * no-mistakes(review): Treat legacy lstart claim starts as indeterminate, not mismatched * no-mistakes(test): Assert legacy handoff interrupts job once * no-mistakes(test): Remove scratch debug test; legacy job case git-tracked * no-mistakes(test): Revert legacy start comparator; simplify legacy handoff test * fix(bin): drop automatic legacy worker upgrade; document manual one-time restart * chore: remove stray file * no-mistakes(document): Drop stale handoff mention from remote worker docs * fix(bin): verify pre-update remote job workers via legacy lstart When a recorded start lacks the proc-starttime= prefix, compare it against ps -o lstart= without LC_ALL=C so a healthy pre-update worker still verifies. The ordinary code-identity path then replaces it once instead of piling supervisors. Add regression coverage for the bare-lstart cookie and the lock upgrade path. * no-mistakes(document): Clarify legacy remote-job start verification docs * no-mistakes(review): Preserve C-locale legacy cookie verification, captain * no-mistakes(document): Clarify legacy process identity cookie compatibility * no-mistakes(lint): Replace deprecated fgrep with lint-compliant grep -F --------- Co-authored-by: Pitch Perfekt Collective <pitchperfekt@Pitchs-Mac-Studio.local> Co-authored-by: mehulbhagwani <270960355+mehulbhagwani@users.noreply.github.com>
…ude (kunchenguid#6722) * fix: use managed-compatible worker approval modes * no-mistakes(document): update codex and claude worker approval mode docs * no-mistakes(ci): Fixed two test files that weren't updated when the PR changed the default claude permission mode from `--dangerously-skip-permissions` (bypass) to `--permission-mode auto`. Changes: - `tests/fm-claude-trust.test.sh` lines 664 and 696: updated two `assert_grep` assertions from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` - `tests/fm-backend-orca.test.sh` line 568: updated one `assert_contains` assertion from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` Both tests now pass locally (orca: exit=0, backend-dispatch/claude-trust: exit=0) * fix: remove stray scratch directories from tests/ tests/.tmp-managed-approvals-test.bBuGhY/ and tests/.tmp-managed-approvals2.exz2qr/ were accidentally committed as worktree-local scratch dirs and do not belong in the repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: preserve bypass defaults with persistent managed approval failover * no-mistakes(review): fix approve-for-me fixture and bypass diagnostic messages * no-mistakes(review): restore absent-Claude-default test and fix pass messages * fix: recognize managed Codex approval prompts in the C locale * no-mistakes(review): Fix managed approval failover bounds, authorization, inheritance, and capture * Simplify managed approval recovery to persistent explicit failover * no-mistakes(review): Guard failover authorization and unlink staging before recovery * no-mistakes(document): Correct managed approval documentation and consolidate owner references * no-mistakes(document): Correct managed-approval references and stale hook guarantees * fix: require approval and inherit managed modes from primary * no-mistakes(document): Clarify live hook verification scope * no-mistakes(review): Remove dead approval_configured key from relaunch metadata * fix: resurface queued wakes from handling successors (kunchenguid#7466) * fix(bin): resurface wakes appended while a handling successor is parked The supervision host parks on the handling successor it starts for a wake its engine handled, and a handling successor never resurfaced recovery, so a row another script appended during the park (a captain inbox note from the Basecamp integration) sat queued until an unrelated close - four hours in the observed case. A handling successor now records the newest queued sequence at its start and closes once with the recovery wake while a newer row is still queued; a newer row already consumed only advances that mark, so the predecessor's delivered episode is still never re-announced. Follow-ups, not built here: - A row appended in the short window between a handled close and its successor's start (about a second) is below the successor's mark; if the engine handles that close without claiming the row, the row waits for the next close. Closing that window needs the host to treat rows left queued after a handled attended turn as main's, which is a separate host change. - After a machine reboot the primary session does not restart on its own, so queued wakes wait until it is resumed by hand. Either auto-resume the primary session after boot, or alert the owner when the primary session is down while wakes are queued. * no-mistakes(review): Close handling-successor startup wake race * no-mistakes(test): Stabilize parked-host inbox wake regression timeout * no-mistakes(document): Clarify handling-successor wake continuity guarantees * no-mistakes(ci): Stabilized the flaky remote secondmate lifecycle test by extending the auto-relaunch watcher deadline from ~30 to ~60 seconds. The CI runner exceeded the old deadline by about 3 seconds; the changed production branch was not reachable in this fixture. `bash -n` and `git diff --check` pass. The full test was attempted but exceeded the 600-second local execution limit before reaching this case * no-mistakes(ci): Fixed flaky concurrent Herdr recovery testing by retrying only documented session-lock contention after both competitors finish. Cleanup now handles fixture-owned read-only hooks. Targeted E2E passed; bash syntax and diff checks passed. Shellcheck reported only existing SC1091 source-following notices * no-mistakes(test): Preserve successor wake boundaries across handoffs * no-mistakes(test): Revert successor boundaries and drain teardown fixture wake * no-mistakes(test): Notify main when downtime handback publication fails * no-mistakes(test): Revert downtime handback notification changes * no-mistakes(document): Correct stale supervision wake routing documentation * fix(bin): stop the supervision host deadlocking and re-announcing handed-back closes Two defects in this branch's handling-successor changes surfaced as the "Behavior portable serial 2" shard hanging at its 30-minute limit. A TERM that lands while the host settles a handled turn's successor handoff left the host holding the wake-queue and recovery-marker locks; its exit cleanup then waited forever on the queue lock it still owned. The cleanup now releases those locks if this process owns them before releasing its grant. When an accepted close turns main-only at its turn, the host hands it back and leaves the successor running, but republished the episode as pending downtime, so that successor re-announced the same close and exited. The handback now republishes the episode as already announced; a later durable append still opens a pending episode the successor recovers. * no-mistakes(test): Park on successor after handling settlement failure * no-mistakes(test): Use tab-delimited handled-settlement failure logs --------- Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com> * fix(bin): preserve accents in pending-reply summaries (kunchenguid#7618) * fix(bin): keep accented letters in pending-reply request summaries fm_pending_reply_summarize deleted every byte outside printable ASCII, so a Portuguese summary lost its accents: "não faça a correção" was stored as "no faa a correo". It now removes control characters only. The 120-character cap counts UTF-8 characters from lead bytes, so it never splits a letter under any caller locale, including the C locale. * no-mistakes(document): Documentar preservação de acentos nos resumos --------- Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com> * feat: add OpenRouter support for typed dispatch resolution (kunchenguid#7149) * Add explicit OpenRouter Jev dispatch resolution * no-mistakes(document): Clarify dispatch documentation and remove stale duplicates * chore: exclude local Stele binding from PR * no-mistakes(document): Correct dispatch help and never-send documentation * ci: retrigger CI after a timed-out test shard * no-mistakes(document): Correct dispatch architecture documentation ownership * feat(bin): publish lane capacity facts with beat pressure and keyed limits (kunchenguid#6983) Opt-in: fm-watch.sh publishes state/lane-capacity.json via bin/fm-capacity.sh --publish only while config/lane-capacity exists; adds beat pressure and keyed limits to fm-capacity.sh. Unconfigured fm-capacity output unchanged. * fix(bin): propagate contribution input assembly failures (kunchenguid#5359) * fix(bin): transport contribution-input JSON through slurpfile, not argjson bin/fm-fleet-snapshot.sh --contribution-input passed the full backlog and task-record JSON to jq through --argjson. A single --argjson argument is capped by the kernel's MAX_ARG_STRLEN (131072 bytes on Linux), so a large enough backlog made the kernel refuse to exec jq while the script still exited 0 with a broken result. Transport both documents through temporary files and --slurpfile, matching the pattern the default snapshot path already uses, and propagate a non-zero exit when that jq invocation fails. Fixes kunchenguid#5349 * test: narrow contribution-input regression to final assembly failure The ARG_MAX transport is already fixed on main by kunchenguid#6830, so keep only the regression proving a failing final jq assembly exits non-zero. * fix: correct Rex secondmate grouping and lifecycle (kunchenguid#7656) * Fix Rex secondmate session lifecycle * no-mistakes(review): Retire Rex secondmate sessions under the owning home identity * no-mistakes(review): Limit Rex FM_ROOT label override to secondmate launches * no-mistakes(document): Clarify Rex secondmate grouping and lifecycle documentation * feat(opencode): add OpenCode 2 support with version-gated launch, plugins, and exit (kunchenguid#7336) * feat(opencode): add OpenCode 2 support with version-gated launch and plugins OpenCode 2 dropped the v1 server-plugin hook API, so the five fm.primary.* plugins now export a default definition with an id and a setup function alongside their legacy OpenCode 1 server export, backed by a new shared lib/fm-opencode2.js helper; the turn-end guard, watch-arm, cd-check, pretool-check, and sessionstart-nudge install their behavior from setup(ctx) on OpenCode 2 and keep the server entrypoint for OpenCode 1. fm-spawn probes the resolved opencode executable's --version once and branches the launch, model flag, reasoning config, exit key, and busy-state plugin off the detected major. A parsed major >= 2 takes the OpenCode 2 shape (no --model, the model pinned through OPENCODE_CONFIG_CONTENT, a raw Ctrl-C exit, the session.execution.* busy plugin); OpenCode 1.x and any undetected or unparseable version fall back to the 1.x shape (--model, 1.x config, /exit) everywhere. The worker busy-state plugin is always provisioned, but OpenCode 1 keeps the pre-port session.status/session.idle plugin so its busy-state wiring is unchanged, and only a major >= 2 gets the session.execution plugin. Adapter and supervision docs, OpenCode 2 live fixtures, and regression tests for the unknown->1.x launch path and the version-gated busy-plugin shape are included. * no-mistakes(review): gate OpenCode 2 busy plugin turn-end touch on notifyTurnEnd * feat(opencode): launch detected OpenCode 2 workers with --standalone OpenCode 2's default launch attaches to the shared background service, which hosts plugins with the service's environment and ignores the pane's OPENCODE_CONFIG_CONTENT. A private server keeps per-worker model, permissions, FM_HOME, and lock ancestry correct. 1.x and undetected versions are unchanged. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * fix(opencode): resolve the OpenCode 2 project location and treat every terminal turn as a turn end OpenCode 2.0.x hands plugins the project only as ctx.location.project.directory and takes ctx.session.prompt({ sessionID, text }), so the V2 setup path never found its root or delivered a prompt. Interrupted and failed turns now end a turn like a succeeded one, while a server shutdown does not. Ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * test(opencode): add a token-free OpenCode 2 native plugin fixture to the live e2e A private opencode serve plus a loopback provider proves the five primary plugins load and act on OpenCode 2: startup nudge, cd and watcher-redirection guards, turn-end guard, watcher arming across succeeded, interrupted, and failed turns. The fixture declares the complete model capabilities object, which OpenCode 2.0.11 requires and 2.0.22 accepts. FM_OPENCODE_LIVE_BINS runs it across several OpenCode 2 executables. The credentialed half now picks its run flags and session tables by detected major, so it keeps working on 1.x. Fixture ported from kunchenguid#5143. Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> * feat(opencode): send one Enter when an OpenCode 2 launch leaves the brief unsent OpenCode 2.0.11 prefills the --prompt brief without submitting it, while 2.0.22 submits it. After launch, fm-spawn waits for the worker's busy-state plugin to report the turn start, sends one Enter only if it has not started and the composer is not empty, and fails the spawn if the turn never starts. * docs(opencode): document the OpenCode 2 launch, delivery gate, and standalone primary homes Records the 2.0.11 and 2.0.22 live evidence for the native plugin fixture and the post-launch brief delivery gate. * no-mistakes(review): Trim OpenCode 2 adapter shapes and harden Ctrl-C exit * no-mistakes(review): Record OpenCode 2 live seatbelt and exit evidence * no-mistakes(test): Document OpenCode 2.0.22 wide-pane sidebar exit refusal limitation * no-mistakes(document): Refresh OpenCode 2 exit-key and seatbelt payload docs * test(opencode): verify OpenCode 1.x live with the dual-export plugins Pin the live e2e TUI panes to the resolved opencode executable, and record OpenCode 1.15.7, 1.18.20, and 1.18.31 evidence for plugin loading, the turn-end guard, watcher continuity, and 1.x worker launch and exit. * no-mistakes(review): Defer OpenCode 2 Enter on unknown composer; widen turn-end docs * no-mistakes(document): Document OpenCode 2 delivery gate unknown-composer test outcomes --------- Co-authored-by: Niranjan <nniranc@gmail.com> Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(bin): resolve the tmux container from firstmate's own session, not the most recent group member (kunchenguid#7662) * fix(bin): resolve the tmux container from the caller's own session, not the most recent group member * no-mistakes(test): Fall back when the $TMUX session id is stale * no-mistakes(document): Document tmux container choice under grouped sessions * fix(bin): read a fully dim Cursor idle placeholder as an empty composer (kunchenguid#7647) * fix(bin): read a fully dim Cursor placeholder as an empty composer cursor-agent 2026.10.19 draws the composer glyph and its idle placeholder as one SGR 2 run with no reverse-video cell, so ghost stripping empties the row and the empty-content guard returned unknown. Relaunch and exit then refused on every idle Cursor pane on tmux. When a styled bare row strips to empty and the plain row is an agent glyph followed by an anchored idle placeholder, read it empty. Bright typed text, other dim text, and unstyled rows keep their existing verdicts. Fixes kunchenguid#7413 * docs(verification): record the fully dim Cursor placeholder verdict and refresh command * fix(bin): say why away-mode inject deferred and pin named Claude idle composer as empty (kunchenguid#6163) * fix(bin): recognize a named Claude session's titled composer rule on cursorless backends `claude -n <name>` right-aligns the session name in the rule above the composer. Cursorless backends (Herdr) accepted only solid rules, so the scan saw the lower rule alone, the idle composer read `unknown`, and away-mode delivery to a named Claude primary deferred until return. A rule carrying one right-aligned title now opens a separator pair. It never closes one, the pair it opens counts only over an agent-glyph row, and it is never a valid Pi pair, so Pi's verdict is unchanged. The away-mode composer deferral now names which refusal it was: unsubmitted text, or no composer shape recognized. * no-mistakes(review): Reword composer deferral reason to cover busy harness * no-mistakes(ci): ci-3 (Greptile, user chose to fix): The rule that must always hold is that Claude's titled opening rule is accepted for any non-empty session name that doesn't start with a space and doesn't contain the rule glyph `─`, including a multiword name. The only place that check lives is the title `case` in `_fm_composer_titled_rule_row` (bin/fm-composer-lib.sh). It now reads `case "$title" in ''|' '*|*─*) return 1 ;; esac`, as the user asked, and the comment above the function mentions multiword titles. I added regression fixtures to `test_matrix_claude_named_session_titled_rule` in tests/fm-composer-lib.test.sh, next to the existing single-word `Andrew` cases. They use the title `Andrew Chen` and check that an idle composer on herdr reads `empty` and a composer with typed text reads `pending`. Something you should know: the old pattern already accepted a title with a space in the middle. It only rejected a title ending in a space. So the new multiword fixtures also pass against the old code, and the bot's failure scenario did not actually happen for "Andrew Chen". What the fix really changes is that it drops the trailing-space rejection, as instructed. Checks run: tests/fm-composer-lib.test.sh and tests/fm-composer-ghost.test.sh both pass (exit 0, no failures), and `shellcheck -x` is clean on both changed files * no-mistakes(review): Match unnamed-ghost fixture rule width to the partner rule * fix: use managed-compatible worker approval modes * no-mistakes(document): update codex and claude worker approval mode docs * no-mistakes(ci): Fixed two test files that weren't updated when the PR changed the default claude permission mode from `--dangerously-skip-permissions` (bypass) to `--permission-mode auto`. Changes: - `tests/fm-claude-trust.test.sh` lines 664 and 696: updated two `assert_grep` assertions from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` - `tests/fm-backend-orca.test.sh` line 568: updated one `assert_contains` assertion from `claude --dangerously-skip-permissions` → `claude --permission-mode auto` Both tests now pass locally (orca: exit=0, backend-dispatch/claude-trust: exit=0) * fix: remove stray scratch directories from tests/ tests/.tmp-managed-approvals-test.bBuGhY/ and tests/.tmp-managed-approvals2.exz2qr/ were accidentally committed as worktree-local scratch dirs and do not belong in the repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: preserve bypass defaults with persistent managed approval failover * no-mistakes(review): fix approve-for-me fixture and bypass diagnostic messages * no-mistakes(review): restore absent-Claude-default test and fix pass messages * fix: recognize managed Codex approval prompts in the C locale * no-mistakes(review): Fix managed approval failover bounds, authorization, inheritance, and capture * Simplify managed approval recovery to persistent explicit failover * no-mistakes(review): Guard failover authorization and unlink staging before recovery * no-mistakes(document): Correct managed approval documentation and consolidate owner references * no-mistakes(document): Correct managed-approval references and stale hook guarantees * fix: require approval and inherit managed modes from primary * no-mistakes(document): Clarify live hook verification scope * no-mistakes(review): Remove dead approval_configured key from relaunch metadata --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Caleb LeNoir <caleb.lenoir@constructease.com> Co-authored-by: Caleb LeNoir <calebl@users.noreply.github.com> Co-authored-by: Alexandre Custodio <xandy.alexandrec@gmail.com> Co-authored-by: Alexandre Custodio <alexandre.custodio@soraschools.com> Co-authored-by: Pedro Guimarães <21346846+0x7067@users.noreply.github.com> Co-authored-by: Konner Moshier <39997550+Moshik21@users.noreply.github.com> Co-authored-by: Oscar Santis Brante <49914973+osantis@users.noreply.github.com> Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Kostadin Pop-Kochev <kostadin.popkocev@gmail.com> Co-authored-by: Niranjan <nniranc@gmail.com> Co-authored-by: Girik Bajaj <bajajgirik2010@gmail.com> Co-authored-by: Tomas Perez <71987424+tomasperezcoscia@users.noreply.github.com> Co-authored-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com> Co-authored-by: Brian <bthome@users.noreply.github.com>
…ounter loss (kunchenguid#6467) * fix: notify handling successors about newer inbox wakes * no-mistakes(document): Document handling-successor inbox regression in watcher coverage list * no-mistakes(ci): Fixed Greptile P1 (comment 4172295712): a handling successor could miss a new inbox wake when `.wake-queue.seq` was missing or malformed. What must always hold: a row added after the successor took its snapshot must be announced and must never be consumed by an older acknowledgement. Two places were breaking this: 1. `bin/fm-watch.sh`: the successor only recorded the highest inherited sequence number. If the counter was reset, a new row could get a number at or below that and be treated as already covered. This now stays silent forever once the inherited rows are acknowledged and the queue drains. 2. `bin/fm-wake-lib.sh` `fm_wake_append_locked`: a reset counter reused the sequence number of a row still in the queue. The predecessor's pending `--ack-through` consumes rows by sequence number, so it deleted the new row before anyone handled it. This was a gap in the existing allocator, and it is what made the before-ack cases fail even after fix 1. Fixes: - Watcher: the successor now snapshots the inherited rows themselves instead of their highest sequence. On each poll it announces if any queued row is not in that snapshot. It still never consumes rows, and inherited rows alone stay silent. - Allocator: the next sequence is now one more than the larger of the counter and the highest sequence still queued, so a new row can't land inside an outstanding acknowledgement's range. - `docs/watcher-continuity.md`: the paragraph and coverage list are updated to match. Tests: `test_append_wakes_handling_successor` in `tests/fm-watch-arm.test.sh` now inherits two rows and takes a counter state (intact, missing, malformed). The four new cases are after-ack and before-ack with a missing or malformed counter, and they run real watcher, arm and drain processes. In after-ack the queue is fully drained before the new append. Evidence: - Before any production change (on 92c724d), all four new cases failed: "not ok - handling successor stranded the second inbox wake" for after-ack and before-ack. - With only the watcher fix, the after-ack cases passed and the before-ack cases still failed, which shows the allocator fix is needed. - With both fixes, all four pass. Full results: `fm-watch-arm` 36/36 (including the intact after-ack and before-ack cases, so ordinary silence and fresh-beacon delivery still hold), `fm-watch-recovery-loop` 2/2, `fm-inbox` 16/16, `fm-pi-watch-extension` 60/60, `fm-wake-queue` 67/67, and all four `fm-wake-drain-*` suites passed (9, 9, 18, 14). Canonical `bin/fm-lint.sh` with `PATH=/home/nino/.local/bin:$PATH` passed: ShellCheck clean, actionlint 1.7.12, 3 workflows valid. That lint run was in local changed-file mode with ShellCheck source following disabled. In a first run of all nine suites in parallel, `fm-wake-queue` failed on "presentation lock delayed the drain for 10s" and one `fm-pi-watch-extension` timing test failed. Both passed in full when rerun on their own, so I read that as load contention, not this change. Design decision to flag: the allocator change sits in the shared wake library, so it goes beyond the watcher comparison the bot pointed at. It is a one-expression change, and the before-ack tests can't pass without it, because otherwise the predecessor's acknowledgement deletes the new message. Not done, per the phase boundary: nothing committed, pushed or posted, and the live fleet was not touched. Evidence logs are in /tmp/fmci, not in a pipeline logs/evidence directory * no-mistakes(ci): Fixed both Greptile findings on PR kunchenguid#6467. Nothing was committed, pushed or posted, and the live fleet was not touched. **ci-1 (4172409813): a new wake was mistaken for an inherited one.** - Invariant that must hold: a successor treats a row as covered only if it is an unchanged inherited row in the inherited recovery episode. - Cause: after the predecessor's acknowledgement and a lost counter, a re-append with the same kind, key, payload and epoch was byte-identical to the snapshot row, so the successor stayed silent. - Fix (`bin/fm-watch.sh`): I used the existing recovery generation rather than a new identity scheme. Any append after an acknowledgement mints a new generation. The successor records the generation with its row snapshot, under the queue lock and in arm-check's lock order. Each poll counts the queue as newer if a row is not in the snapshot, or if the queue is non-empty and the generation changed. The existing arm-check then decides whether to announce. No rows are consumed, and inherited rows alone stay silent. - The row snapshot is still needed for before-ack, because an append before the acknowledgement keeps the same generation. **ci-2 (4172409821): large sequence numbers became unusable.** - Invariant that must hold: sequence numbers are always exact digit-only decimals. - Fix (`bin/fm-wake-lib.sh`): the allocator picks the largest of the counter and the queued sequences by comparing decimal text (length, then string order, leading zeros stripped), then adds 1 in bash with `$((10#$seq + 1))`. This restores the 64-bit exactness of the original bash counter. - Same fix at two places in `bin/fm-wake-drain.sh` that print sequences: `presented_max_row` and `ACK_THROUGH` print the matched text instead of `max + 0`. **Tests** - `tests/fm-watch-arm.test.sh`: two new `identical` cases, after-ack with a missing and with a malformed counter. A fake `date` pins the epoch, and the test asserts the row is byte-identical, then checks timely notification, preservation until its own acknowledgement, and no repeat notification. - `tests/fm-wake-queue.test.sh`: new `test_large_sequences_stay_exact_decimal_identifiers` starts from 2147483647, 9007199254740991 and 9223372036854775806, each with an intact, missing and malformed counter. It checks the next sequence is exact and digit-only. For the first two it also checks a real drain shows both rows, names the exact acknowledgement sequence, and acknowledging empties the queue. - `docs/watcher-continuity.md` now describes the generation check. **Evidence** (in `/home/nino/.no-mistakes/evidence/01M40CDTX2000A5TFFRZN27YFR/ci-round3/`) - Before, on 050a5fc: both identical cases failed with "handling successor stranded the second inbox wake (after-ack)". The large-sequence test failed with "advanced to '9223372036854775808', not 9223372036854775807". - After the fix, all three pass (`after-*.log`). - All nine suites passed, each run on its own, all with 0 not ok (`final-results.txt`): `fm-wake-queue` 68/68, `fm-watch-arm` 38/38, `fm-watch-recovery-loop` 2/2, `fm-inbox` 16/16, `fm-pi-watch-extension` 60/60, `fm-wake-drain-open-decisions-cursor` 9/9, `fm-wake-drain-open-decisions` 9/9, `fm-wake-drain-outcome-backstop` 18/18, `fm-wake-drain-unread-status` 14/14. - `bin/fm-lint.sh` with `PATH=/home/nino/.local/bin:$PATH` passes: ShellCheck clean, actionlint 1.7.12, 3 workflows valid (local changed-file mode). **Not run** - The only OpenCode suite is an opt-in live test, so I did not run it. **Limits** - The 2.14748e+09 symptom doesn't reproduce with the awks here (gawk, mawk 1.3.4, busybox). The failure reproduced locally is the precision loss at 64-bit values. - The drain's awk comparisons are only exact up to 2^53. That predates this PR and I left it alone, so the drain test stops at 2^53 * no-mistakes(document): Reattach successor poll-loop sentence to its subject
… directory is removed (kunchenguid#7685) * fix(tests): bound lock waits on removed dirs and reap fixture-root processes A lock wait spun every 0.1s forever once its directory was removed, so a watcher or reconcile scan of a deleted test home never reached its own home-gone exit. fm_lock_acquire_wait now returns within 0.2s once the lock's parent directory is gone. Test teardown now signals every process naming the exact fixture root (TERM, then KILL) before removing it, in both fm_test_cleanup and the orphan sweep. The supervision-host fake harness sessions stop once their home is gone or the suite pid has died, so a hard-killed runner no longer leaves them polling under init. Fixes kunchenguid#7672 * fix(tests): bound the session-lock concurrency barrier Forty workers polled a find pipeline every 0.01s to wait for each other, so the barrier alone launched thousands of processes a second, and once a stopped run removed the barrier directory the workers polled forever under init. Count entries with a builtin glob every 0.05s, and end the wait once the directory is gone, the suite has died, or the stub block bound passes. Refs kunchenguid#7672
The merged fork and upstream tests no longer pack into nine shards under the 20-minute budget.
The fork's archive-configuration regressions need a quoted value that contains a hash and a spaced table header to read as tasks-axi reads them.
An answered legacy row cannot vouch for a call whose own row was archived unanswered.
The fork's inbox grace replaced the stuck-busy escalation that the upstream live-proof text describes.
Before and after test runsTest runs of seven suites on fork main before the merge and on the PR head after the merge. Each run is
PR gate suite (fork-only, kept)The PR gate suite tests a fork-only check. It must pass before the merge and after the merge. Before (fork main 244d836) - Marked line: the suite passes before the merge. After (PR head e0112f6) - Marked line: the suite passes after the merge. Secondmate stand-down suite (fork-only, kept)The secondmate stand-down suite tests a fork-only rule. It must pass before the merge and after the merge. Before (fork main 244d836) - Marked line: the suite passes before the merge. After (PR head e0112f6) - Marked line: the suite passes after the merge. Bitbucket CI-ready and named-head suite (fork-only, kept)The Bitbucket CI-ready checks are fork-only. They must pass before the merge and after the merge. Before (fork main 244d836) - Marked line: the suite passes before the merge. After (PR head e0112f6) - Marked line: the suite passes after the merge. Brief generation suite (fork and upstream text)The brief suite tests fork text and upstream changes. It must pass before the merge and after the merge. Before (fork main 244d836) - Marked line: the suite passes before the merge. After (PR head e0112f6) - Marked line: the suite passes after the merge. Captain-hold lifecycle suite (upstream adds cases)The merge adds new captain-hold cases. The suite must pass before the merge and after the merge. Before (fork main 244d836) - Marked line: the suite passes before the merge. After (PR head e0112f6) - Marked lines: the merge adds these test cases. Watcher wedge-cap suite (new from upstream)The wedge-cap suite comes from the upstream merge. It is new in the merge. Before (fork main 244d836) - Marked lines: the run finds no suite. After (PR head e0112f6) - Marked line: the new suite passes after the merge. Rex backend suite (new from upstream)The Rex backend suite comes from the upstream merge. It is new in the merge. Before (fork main 244d836) - Marked lines: the run finds no suite. After (PR head e0112f6) - Marked line: the new suite passes after the merge. |














Intent
fm-maint, prepare a pr getting updates from upstream
Context: one PR on cloud-practitioner/firstmate that brings everything new on kunchenguid/firstmate main since upstream 83b70ef (the upstream tip the previous merge-upstream, fork PR #62, merged) into fork main, the same way as that PR: a real merge of upstream main into a branch from current fork main, never GitHub "Sync fork", a rebase or a squash, with conflicts resolved so every fork-only feature and commit is kept.
Fork-only features that must survive include the PR evidence gate (fork PR #64), the durable stand-down and resume for local secondmates (fork PR #65), the live Claude launcher isolation (fork PR #63), the
contrib/iqx-ixia-hook/folder, the Bitbucket CI-ready recognition and named-head gating, the per-home task temp root, the Bitbucket PR checks, merge guards and merge-message option, and the other fork-only fixes already on fork main.It must land as a merge commit, never a squash or rebase, so fork history stays intact, and the PR description says so.
Upstream is only fetched: nothing is pushed, opened, filed or commented on kunchenguid/firstmate; the PR is opened on cloud-practitioner/firstmate only, runs the fork's validation path, and is not merged as part of this work.
The report gives the PR URL, the upstream range merged, and any conflict that needed a judgment call, including any where keeping a fork feature meant dropping or reshaping an upstream change.
The PR description's risk assessment describes the final PR head, with no stale findings from before a fix.
When planned, upstream main was af1c681, 36 upstream PRs ahead of 83b70ef:
kunchenguid#7552, kunchenguid#7407, kunchenguid#7364, kunchenguid#7631, kunchenguid#7630, kunchenguid#7633, kunchenguid#7625, kunchenguid#6588, kunchenguid#7636, kunchenguid#7639, kunchenguid#7634, kunchenguid#7649, kunchenguid#5688, kunchenguid#7530, kunchenguid#7637, kunchenguid#6094, kunchenguid#7627, kunchenguid#7650, kunchenguid#7466, kunchenguid#7618, kunchenguid#7149, kunchenguid#6983, kunchenguid#5359, kunchenguid#7656, kunchenguid#7336, kunchenguid#7662, kunchenguid#7647, kunchenguid#6163, kunchenguid#7666, kunchenguid#5947, kunchenguid#6300, kunchenguid#7421, kunchenguid#6440, kunchenguid#6722, kunchenguid#6467, kunchenguid#7685.
What Changed
kunchenguid/firstmatemain (83b70ef4..af1c6817) through a real merge into the fork branch. This PR must land with a merge commit, never squash or rebase, to preserve fork history.Merge method
This PR must land as a merge commit, never a squash or a rebase, so fork history stays intact.
The branch holds a real
git merge --no-ffof upstreamkunchenguid/firstmatemain (af1c6817, the second parent) into fork main (244d8368, the first parent) as02c9c410, followed by the follow-up commits listed below.It is not GitHub "Sync fork", a rebase or a squash.
Upstream was only fetched by URL; nothing was pushed to, opened on, filed on or commented on
kunchenguid/firstmate.Upstream range merged
Range
83b70ef4..af1c6817(the previous merge, fork PR #62, ended at83b70ef4): 36 upstream commits, 36 upstream PRs, upstream tip unchanged since planning.Conflicts and resolutions
27 files conflicted. Default was to keep both sides; where upstream shipped its own version of a fork change, upstream's implementation was taken.
.agents/skills/afk/SKILL.md,CONTRIBUTING.md: upstream's shorter backend and supervisor-pane wording (both sides rewrote the same line to the same anchor)..agents/skills/harness-adapters/references/harness/kimi.md: the fork's one-sentence-per-line text, plus a line saying Rex is also in the viewport-capture set.README.md: upstream's Rex-aware "Restart-proof" line and the fork-only "PR evidence" feature line.bin/fm-backend.sh:fm_backend_agent_statekeeps the fork's expected-label argument for herdr and dispatches rex to upstream's classifier.bin/fm-config-inherit-lib.sh: upstream's inheritable list (addscodex-approval-mode) plus the fork'spr-evidence.bin/fm-control-lib.sh,bin/fm-control.sh,bin/fm-spawn.sh,bin/fm-crew-state.sh: see judgment call 3; the spawn meta key list is upstream's plus the fork'sherdr_process_identity.bin/fm-remote-job-lib.sh,bin/fm-remote-job-worker.sh: upstream's whole implementation taken (superseded list below).bin/fm-test-run.sh: union of the pr-forge family list (fork Bitbucket suite, upstream PR-health suite).docs/agent-control.md,docs/architecture.md,docs/scripts.md: both sides' additions merged (fork PR-identity, Herdr restart pointer, non-dormant liveness wording, evidence rows; upstream Rex wording and PR-health row).docs/configuration.md: upstream's lane-capacity, PR-ready-after-green and task-status-archive sections and Rex backend lines.docs/verification/runtime-backends.md: the fork's busy-inbox verification notes kept, because the fork's inbox grace replaced the stuck-busy escalation upstream's live-proof text describes; upstream's OpenCode 2 and OpenCode 1 sections kept.tests/lib.sh: upstream's fixture-root process reaping kept; the fork's tree removal stays intests/fixture-tree-helpers.sh; cleanup reaps, then removes spawn launch dirs, then the tree.tests/fm-backend-herdr-presentation-e2e.test.sh: upstream's tolerant concurrent-recovery helper plus the fork's slow-lock-holder serialization assertions (real-Herdr suite, not run here).tests/fm-backend-herdr.test.sh: the fork's stopped-server piped-reader test with upstream'sfm_run_timed.tests/fm-remote-secondmate-lifecycle-e2e.test.sh: the fork's larger wait bound.tests/fm-supervision-host.test.sh: upstream's case list, each wrapped in the fork's per-case cleanup, plus the fork's downtime-write-failure case.tests/fm-test-fixture-cleanup.test.sh: both case calls kept.Judgment calls (including where a fork feature reshaped upstream)
#inside a quoted value as data and accepts a spaced table header such as[ markdown ]; and durability is judged per identity, exact id first, so an earlier answer on a live legacy-derived row cannot vouch for a call whose own row was archived unanswered.PORTABLE_SERIAL_SHARDSis now 10 inbin/fm-test-run.sh, with the matching shard matrix in.github/workflows/ci.yml, the required-context list inCONTRIBUTING.mdand one doc sentence. Operator action: addBehavior portable serial 10to any branch-protection required-check list that names serial 1 to 9.fm_control_endpoint_absence_verdictthird argument is backend specific: herdr takes the fork's expected pane label, rex takes the task meta (upstream's rex proof readsrex_serverfrom it). Callers infm-control exitandfm-spawn --relaunchpass the matching value. This keeps upstream's rex test contract and the fork's herdr contract unchanged.Fork-only changes upstream now supersedes
fm-remote-job-lib.shandfm-remote-job-worker.shwere taken whole, the fork's fixture tests still pass.fm_run_timed(test: remove GNU timeout dependency from herdr backend test kunchenguid/firstmate#7636); the fork's stopped-server assertions are kept.Known upstream issues carried unchanged
Fleet-affecting changes
cistep that ends on "no CI checks reported" reads as working, and a worker's own "checks green" report is not promoted while that marker is latest. The fork's GitHub Actions has not started a run since 2026-10-09, so every fork PR shows no checks: supervision keeps a fork PR in working and never raises the checks-green signal from CI. Fork PRs are treated as ready from the worker's explicit done line after the CI step has shown only "no CI checks reported" for 10+ minutes with the PR head equal to the pushed head. Upstream's reading was not weakened.rexbackend, auto-selected when firstmate itself runs inside a Rex block (TERM_PROGRAM=rexwithREX_BLOCK); needs therexCLI andjqon such homes only. Rex push notifications, cursor-aware composer detection and secondmate grouping fixes.opencode_versionin task meta), plugins and exit handling.config/codex-approval-modeandapproval_modemeta key;codex-approval-modeis now inherited by secondmate homes.config/usage-warnerthresholds.config/lane-capacitygains optional keyed lines (pressure-file,reserve-flag, an input-idle key,quota-provider, limits); with the file present the watcher publishesstate/lane-capacity.json. No change with the file absent.FM_DISPATCH_PROVIDER=openrouterwith a non-emptyFM_DISPATCH_MODEL; never selected by the presence of a key.FM_WEDGE_MAX_ESCALATIONS; feat(bin): archive task status history on teardown behind an opt-in flag kunchenguid/firstmate#7407config/status-archive; feat(bin): re-raise unanswered PR review threads after an opt-in reply age kunchenguid/firstmate#7364FM_CONTRIBUTIONS_REPLY_AGE; feat(bin): refuse unguarded Herdr lifecycle briefs at spawn behind an opt-in flag kunchenguid/firstmate#7631config/herdr-lifecycle-spawn-guard; fix(bin): withhold a child's PR-ready line until checks green behind an opt-in flag kunchenguid/firstmate#7630config/ready-after-green; feat(bin): notify owned PR conflicts and failing required checks behind an opt-in flag kunchenguid/firstmate#7634config/pr-health-monitor.Behavior portable serial 10to required checks if the fork lists serial 1 to 9.Commits added after the merge commit
Four by hand (tenth shard; quoted-value TOML handling; per-identity durability order; busy-inbox notes) and two by the pipeline:
6ed83546(review fixes R2 and R3 above) ande0112f67(documentation refresh: wording corrections indocs/, comment-only edits inbin/fm-pr-check.sh, and rewrites ofdocs/opencode-2-plugin-port-scope.mdanddocs/examples/usage-warner, which the merge itself did not change).Pre-existing failures (not in this PR)
tests/fm-backend-herdr.test.shfails on plain fork main attest_secondmate_probe_cannot_borrow_another_tasks_binding(STATEunbound infm-secondmate-liveness-lib.sh, from fork PR 65). Every other case of the suite passes on this merge when that one call is removed.tests/fm-remote-transport-lanes.test.shfails on plain fork main: its copiedremote-root/binlacksfm-secondmate-dormant-lib.sh, whichfm-ff-lib.shnow sources (fork PR 65).tests/fm-lint.test.shalso fails on plain upstreamaf1c6817(host-specific). Suites needing ruby (fm-test-run,fm-ci-workflow,fm-nm-test-contract) or tmux (fm-spawn-orca-worktree,fm-gate-refuse) cannot run on this host.fm-remote-jobandfm-watcher-lockflaked once and passed on rerun.Screenshots
This merge changes neither a UI nor an HTTP API, so it needs no before and after screenshots.
Risk Assessment
e0112f67(merge commit plus six follow-up commits). The merge touches dispatch, supervision, endpoint recovery and the test runner across 36 upstream PRs, but the review found no unresolved defect after the two fixes (R2, R3) and the recorded scope decision below.Testing
The prior payload reports that the specified suites ran once, serially and in order, followed by successful touched-shell syntax and coverage-registration checks; no reruns were needed. Prior scratch was removed and no untracked files remained. Optional Beads-specific held-call cases were capability-skipped by the installed markdown-only tasks-axi. Retained transcripts are fixture-based evidence, not live product proof. Live validation was excluded under the reported operator-approved scope. All nine scenarios are therefore untested for live-validation purposes; fixture success does not establish a live pass.
Evidence: Captain-hold lifecycle regression transcript
Source: Captain-hold lifecycle regression transcript
Evidence: Watcher wedge-cap regression transcript
Source: Watcher wedge-cap regression transcript
Evidence: Backend selection and lifecycle fixture transcript
Source: Backend selection and lifecycle fixture transcript
Evidence: Rex adapter stateful-fake regression transcript
Source: Rex adapter stateful-fake regression transcript
Evidence: Brief-generation regression transcript
Source: Brief-generation regression transcript
Evidence: Named-head and CI-ready completion-gate transcript
Source: Named-head and CI-ready completion-gate transcript
Evidence: Repository publication-authority regression transcript
Source: Repository publication-authority regression transcript
Evidence: Fixture cleanup and ownership regression transcript
Source: Fixture cleanup and ownership regression transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed ✅
bin/fm-captain-hold.sh:1237- The previously approved correction is still absent. fm_backlog_pr_note_label succeeds for every URL, including supported GitHub and Forgejo PRs, so this branch bypasses the structured-artifact update at bin/fm-captain-hold.sh:1241-1242 for every --pr marker. If teardown records retention but fails returning the worktree, answering before cleanup replay closes the task with only a note instead of restoring its structured PR link. Restore the fm_backlog_row_artifact_supported condition; use the label helper only for unsupported URLs, preserve the --report path, and cover answering a supported retained PR before replay.tests/fm-watch-wedge-cap-live.test.sh:282- The previously approved acknowledgement correction is still absent. Draining without the generation-bound --ack-through command leaves the first wake queued; subsequent watcher cycles can report check: rearm-resurface rather than advancing the wedge ladder. The live guard counts any nonempty reason as an escalation at tests/fm-watch-wedge-cap-live.test.sh:274-276 and carries outstanding recovery state into the capped arm at :290. The same invariant is violated between invalid-override rounds at tests/fm-watch-wedge-cap.test.sh:690-705, where recovery wakes can satisfy the assertions without another wedge escalation. Acknowledge each presented wake between independent rounds and require actual numbered wedge-escalation outcomes, while preserving intentionally unacknowledged queue scenarios elsewhere.🔧 Fix applied.
✅ Re-checked - no issues remain.
git -C ~/.no-mistakes/worktrees/450411b3e67c/01M5NDSRSC3QSFVJQR0FE670EK statusandgit -C ~/.no-mistakes/worktrees/450411b3e67c/01M5NDSRSC3QSFVJQR0FE670EK diff). Respond with fix to validate it, or abort.🔧 No changes applied.
1 warning still open:
Removed the untracked.test-phase/directory and confirmed nol.*lab directories remained.Ran all validation commands underenv -i PATH="$PATH" HOME="$HOME" USER="$USER" LANG=C.UTF-8 TERM=dumb NO_MISTAKES_GATE=1, leaving TMPDIR, fleet identity, task overrides, and forge credentials unset.bin/fm-test-run.sh --jobs 1 tests/fm-captain-hold-lifecycle.test.shbin/fm-test-run.sh --jobs 1 tests/fm-watch-wedge-cap.test.shbin/fm-test-run.sh --jobs 1 tests/fm-backend.test.shbin/fm-test-run.sh --jobs 1 tests/fm-backend-rex.test.shbin/fm-test-run.sh --jobs 1 tests/fm-brief.test.shbin/fm-test-run.sh --jobs 1 tests/fm-dod-lib.test.shbin/fm-test-run.sh --jobs 1 tests/fm-self-repo-role.test.shbin/fm-test-run.sh --jobs 1 tests/fm-test-fixture-cleanup.test.shRanbash -non every changed shell file between244d8368fb436e31962c91fc2d60cb2c7d78f16eand6ed8354606febe990a522245fe49ed7e6fb10b86, including the live wedge guard without executing it.bin/fm-test-run.sh --check-coverageConfirmedgit status --shortandgit diff --statwere empty after validation.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.