Repository navigation
Conversation
… work (kunchenguid#7352) * feat(bin): add opt-in config/spawn-base-primary-checkout for local landings A fresh pooled task worktree is reset to origin/<default>. A local-only project lands work on the primary checkout's default branch through fm-merge-local.sh and never pushes, so that ref stays frozen at the last push and every fresh task starts behind work already landed, loading the project's instruction files from the stale tree. The always-on fix proposed in kunchenguid#3812 replaced the specified origin-only refresh for every home; this rework keeps that rule as the default and moves the new base behind an explicit, default-off presence flag, following the existing config/<flag> pattern. With config/spawn-base-primary-checkout present: - fm-spawn.sh starts a fresh local-only ship, or a scout without --base-branch, from the primary checkout's <default> when it strictly contains origin's tip. Publishing modes and named base branches keep origin's tip, because that work is delivered against origin. A diverged primary keeps origin's tip and prints a note counting both sides. The commit is read from the local object store, so nothing extra is fetched, and the clean-slot refusal still runs first. - fm-review-diff.sh anchors a remote-backed task without base_branch= on the branch it delivers to: publishing modes stay on origin/<default>, while a local-only ship or a scout is compared against whichever of the local <default> and origin/<default> meets the branch later, so the review shows the task's own work instead of every locally landed commit. - fm-promote.sh tells a scout promoted to a publishing mode to return to origin's default branch, because in this home the scout may have started from the primary checkout's branch, whose unpushed commits would otherwise be published with the work. fm-dod-lib.sh gains fm_mode_publishes as the one predicate those three readers share. Without the flag every path is unchanged: the spawn resets to origin's tip, the review anchors on origin/<default>, and promotion keeps its wording; the existing tests run unmodified. Treehouse 3 (verified on 3.1.3) already hands a slot out at whichever of the local and remote default branch is further ahead, so with the flag the refresh keeps that slot where Treehouse put it for a local landing instead of rewinding it, and a project without origin is untouched as before. Supersedes kunchenguid#3812. * no-mistakes(document): Drop change-relative wording from spawn-base config docs * no-mistakes(ci): The Greptile Review check failed on two review findings, not on a broken build or test. I fixed both as the user directed, and the tests and lint pass. ci-1 (bin/fm-promote.sh): promotion must name origin's default branch only when spawn could actually have started the scout from the primary checkout. Spawn does that only when the project has an origin remote and is recorded on the task. Before this fix, the opt-in flag alone made promotion to no-mistakes or direct-PR say "copy of origin's default branch...", even on a project with no origin. - The new wording now also requires a recorded project (`[ -n "$PROMOTE_PROJECT" ]`) whose `git remote get-url origin` succeeds. That is the same check bin/fm-review-diff.sh already makes. The empty-project guard is needed because `git -C ""` would run in the current directory. Otherwise the existing "default-branch base" wording stays. - The three places that read the flag now agree: spawn, review-diff and promote. - I updated the header and inline comments in fm-promote.sh, and the promote line in docs/configuration.md, to say this applies only to a project with an origin remote. - Test: `test_promote_names_origin_for_a_publishing_mode_in_an_opted_in_home` in tests/fm-task-delivery.test.sh now records a real `project=` with a registry entry, and covers flag on/off × origin/none × direct-PR/no-mistakes/local-only. Only flag on + origin + a publishing mode gets the new wording. The named-base case is unchanged. - Before the fix, the test fails with "not ok - flag on, remote none, direct-PR: the promotion changed the default-branch wording". With the fix, it passes. ci-2 (tests/fm-spawn-pool-base-freshen.test.sh): with the flag set, both publishing modes must start from origin's tip and print no divergence note. - The opted-in spawn test now covers no-mistakes as well as direct-PR, local-only and scout. - The expectations no longer single out direct-PR. Both publishing modes now expect origin's tip, no "note: pooled worktree", and a review anchored at the slot's start, in the leading, diverged and trailing cases. Verification: - `bash tests/fm-task-delivery.test.sh`: exit 0. - `bash tests/fm-spawn-pool-base-freshen.test.sh`: exit 0. - `bin/fm-lint.sh` on the three changed shell files: exit 0 (ShellCheck 0.11.0, pinned, with extended analysis)
…enguid#7915) * fix(teardown): distinguish unavailable content-landed proof * no-mistakes(document): Clarify teardown proof failure documentation * no-mistakes(ci): Captain, CI failed in unchanged watcher recovery code, independent of this PR. The focused failing case passed locally with exit 0. The full suite stalled earlier and was stopped. No tracked changes. Command guard rejected cleanup; scratch remains at .ci-7915/ and tests/.ci-7915-cycle.sh * chore: untrack CI validation scratch
…kunchenguid#7876) * feat(dispatch): prefer configured provider family * no-mistakes(review): drop policy_why field and dead auth fixture from preference tests * no-mistakes(document): docs: record preferred_provider in schema, diagnostics, verification
) * fix: pin local-only landing to its accepted done head Persist the accepted local-only commit in task metadata and refuse landing when the branch or the recorded head changes. Merge the full commit ID under the task control and metadata locks. Closes kunchenguid#7782 * no-mistakes(test): Refuse local landing races with atomic ref transactions * no-mistakes(document): Document pinned local landing and recovery contracts * no-mistakes(ci): Applied only the verified incremental patch and preserved existing repairs. Complete diff matches the supplied candidate hash. Both focused test files, default lint, and source-aware lint passed with exit 0. Evidence retained at /tmp/up7782-local-repair/ci-phase-fresh-*. No commits, pushes, or pipeline-control actions performed; publication remains with the outer executor * no-mistakes(ci): Fixed both SC2031 false positives in tests/fm-merge-local.test.sh with narrowly scoped suppressions. Tier 2 lint correction; runtime changes were unnecessary. Default lint, source-aware lint, all 8 focused tests, and git diff --check passed. No commits, pushes, or pipeline-control actions performed
…henguid#7928) * fix(procevent): coalesce unhandled extension host errors * no-mistakes(document): Document host-error coalescing and recovery semantics
…uid#7912) * fix: stop wake grants when lock acquisition fails * no-mistakes(document): Document wake grant lock acquisition failure behavior
* fix: send remote steers over stdin * fix: version remote stdin send protocol * no-mistakes(document): document stdin transport for remote steer in fm-send header * no-mistakes(ci): Two tests failed in CI. One is caused by this PR; the other is not. 1. Behavior portable serial 5: `tests/fm-secondmate-reconcile.test.sh` failed with "supervision did not deliver the durable reconcile request later". This PR caused it. The PR now sends a remote steer's body on stdin, using `fm-on.sh --stdin` with the `send-stdin` verb. This test's fake ssh started with `cat > /dev/null`, so it threw the body away before it ran the real `fm-remote-secondmate-control.sh`. `cmd_send_stdin` then read an empty message, died with "remote steer message must be nonempty", and no inbox record was written. Rule that broke: a fake ssh that runs the real remote leg must pass the caller's stdin through, as real ssh does. I checked every fake ssh that runs the real leg. `fm-send-remote-delivery.test.sh` already passes stdin through. `fm-secondmate-sync.test.sh` (`make_remote_leg_ssh_stub`) had the same discard line. The stub in `fm-send-resolve-key.test.sh` never runs the leg, so it is fine. The fix deletes the `cat > /dev/null` line in the reconcile and sync stubs. Calls made without `--stdin` still get /dev/null from fm-on, so nothing else changes. Checks run locally: the reconcile test fails before the fix with the same CI message. After the fix, the reconcile test passes (rc=0) and the sync test passes (rc=0). 2. Behavior portable serial 3: `tests/fm-wake-queue.test.sh` failed in `test_concurrent_append_and_drain` with "wake drain: queue lock could not be acquired safely". This PR did not cause it. The PR does not touch `bin/fm-wake-drain.sh`, the wake tests or the wake helpers. The failure is lock contention: 40 appenders run at the same time as a drain, on a loaded CI runner. Locally the whole test passed (70 ok, rc=0). I made no change for it. It is an existing flake in wake-queue lock acquisition and needs its own fix outside this PR. Open PR kunchenguid#7912 ("preserve wake grants when queue-lock acquisition fails") works in the same area
…owner manages (kunchenguid#7357) * fix(herdr): never start a server an external owner manages or is replacing fm_backend_herdr_server_ensure started a detached herdr server whenever status reported the session not running. When a service manager owns the server, that races a unit restart or a server live-handoff and splits panes across two servers. The session directory reported by herdr session list can now carry a firstmate-server-owner marker (persistent, naming the owner) and a firstmate-server-replacing marker (for the duration of a restart or handoff). While either exists and the server is not running, server_ensure never starts a server: it waits a bounded time (FM_BACKEND_HERDR_EXTERNAL_SERVER_POLLS, default 30s) for the server to report running, and otherwise fails naming the owner or the replacement. A running server is still used at once, and a session with no marker keeps the Firstmate-owned autostart unchanged. Fixes kunchenguid#7320 * no-mistakes(review): Enforce herdr replacement marker on fast path; fixed wait bound * no-mistakes(ci): Both CI failures had one cause in this PR, and I fixed it in code; no test assertions changed. `fm_backend_herdr_server_ensure` had started running `herdr session list --json` on every call to find the session directory for the ownership markers. Two existing tests treat that call as a sign of something else: - `tests/fm-control-relaunch.test.sh` failed with "a herdr reclaim touched a session the record does not name: session list --json", because it allows only `--session fmlab` calls and `status --json`. - `tests/fm-backend-herdr-presentation-e2e.test.sh` failed with "opted-out spawn added presentation-ordering capability or socket calls", because there `session list` means presentation ordering ran. Invariant: `server_ensure` must not make Herdr calls beyond its own `status` read, so ordinary spawns and reclaims stay free of `session list` and only touch the session they name. Every caller goes through `server_ensure`, so fixing it there covers all of them. Fix (`bin/backends/herdr.sh`): the session directory now comes from the `status --json` read `server_ensure` already makes. It is the directory containing `.server.socket`. I removed the `fm_backend_herdr_session_dir` helper. On a running server, the only added work is one check for the replacement marker, which is what the user asked for. I checked this against real Herdr 0.9.5: - `status` reports the socket even when the server is stopped, for both the default and named sessions. - The socket's directory is the same `session_dir` that `session list` reports, and it follows `XDG_CONFIG_HOME`. - With `--session`, an inherited `HERDR_SOCKET_PATH` does not change it. - The read creates nothing on disk. If a Herdr version omits the socket, `server_ensure` keeps the old Firstmate-owned behavior, as the docs already said for a missing `session_dir`. Other changes: the marker-test fakes now report the socket under the session directory, and I removed the `session list` stub this PR had added to the shared fake. `docs/herdr-backend.md` and `docs/verification/runtime-backends.md` ("Session directory") now describe and record the `status`-socket measurement. Verification: - `tests/fm-backend-herdr.test.sh` passes: 239 ok, 0 not ok, including all the externally-managed and replacement-marker tests. - `tests/fm-control-relaunch.test.sh` passes; it was the shard 3 failure. - shellcheck shows only existing info-level notes. Not verified locally: the real-Herdr presentation test. Here it stopped earlier, at an unrelated Treehouse timeout ("treehouse get did not enter an isolated worktree within 60s"). After the fix `server_ensure` no longer runs `session list`, which is the call that assertion caught. No lab session was left running, and I removed the test's temp directory
…mputes (kunchenguid#7931) * fix(bin): reclaim a lock whose recorded pid-identity no longer recomputes kunchenguid#7886 made fm_lock_holder_alive reject a live pid whose start time no longer matches the lock's recorded pid-start. A holder that predates that record still falls back to pid liveness, which leaves issue kunchenguid#7801 open: on a /proc-less host outside UTC, a watcher started before fm_pid_identity pinned TZ=UTC0 recorded a local-time pid-identity. After the update that live holder no longer matches its own identity, so the health predicate rejects it, yet every new watcher stands down beside it and the home stays unsupervised. fm_lock_holder_alive now also requires a recorded pid-identity, when present, to still recompute for the pid, after the existing start-time check and with the same missing-evidence fallbacks: no record, a pid rewritten mid-read, or an unreadable value keeps the pid-liveness answer. Both records go through one helper, _fm_lock_record_matches. Only long-lived holders that never exec write a pid-identity (the watcher, the supervise daemon), so a genuine holder still matches. A watcher replaced this way stands down unsignalled at its next poll through the existing self-eviction. The fm_pid_identity comment and docs/turnend-guard.md claimed the upgrade-window residual is cleared by the post-update restart, which never reaches a primary home's own watcher; they now say the next lock claim clears it. Verification: - Drove: real bin/fm-watch.sh processes with /proc hidden through FM_PROC_ROOT_OVERRIDE and TZ=America/Los_Angeles: a live genuine watcher whose lock record was rewritten to its own local-time ps lstart identity with no pid-start, as a pre-pin build wrote it, then a fresh watcher; plus fm_lock_try_acquire against a live pid with a mismatched, matching, and unrecorded pid-identity and no pid-start. - Observed: on the unchanged library the fresh watcher exits "lock held by live pid ... but heartbeat is stale" and fm_lock_try_acquire returns held for the mismatch. With this change the fresh watcher takes the lock, the legacy holder exits 0 by standing down, the live pid is never signalled, and matching or unrecorded identities stay held. - Re-check: tests/fm-watcher-lock.test.sh cases test_watch_replaces_live_holder_with_legacy_local_time_identity and test_lock_reclaims_holder_whose_identity_no_longer_recomputes. * no-mistakes(review): Limit pid-identity lock reclaim to the watch lock * no-mistakes(document): Correct fm_lock_holder_alive comment on non-watch identity locks * no-mistakes(ci): I made two small fixes. CI and "Require no-mistakes" never ran: GitHub shows both as `action_required`, waiting for approval to run workflows from a fork, so the code didn't cause those. Greptile failed because it scored the PR 4/5 against a required 5/5, with one P2 comment. 1. **Greptile P2, test children left running (`tests/fm-watcher-lock.test.sh`).** The two new lock tests start a `sleep 300` holder that only gets killed after every assertion passes, and the suite's cleanup only reaps processes whose command line contains the fixture root. Each holder now starts as `(exec -a "$dir/holder" sleep 300)`, so cleanup reaps it if the test fails early. To check, I forced a `fail` right after the child starts: on HEAD the `sleep 300` was still running afterwards, and with the fix it was reaped. Both tests pass normally. 2. **Flaky takeover test (`bin/fm-watch.sh`).** This edit was already uncommitted in the worktree. Twelve isolated runs passed without it, so I first dropped it as out of scope, but the full suite then failed on exactly this race. When a successor takes over a live watcher's lock, it deletes the lock and recreates it while holding `.watch.lock.steal`. If the old watcher checks during that gap, it sees no lock `pid` and exits as "state directory was torn down", not by standing down cleanly as the PR intends. That race only happens once this PR lets a healthy, live watcher's lock be taken. The guard treats a missing `pid` as teardown only when no `.steal` marker exists, with a comment explaining why. With it restored, 15 runs of the takeover test (three at a time, to add load) all passed, and all three PR tests passed in the full suite. **Remaining failures:** with the guard restored, I ran the full suite three times and it failed each time, but always on tests this change doesn't touch: "bounded child cleanup regression" twice and "bounded ledger cycle 2 did not start" once. The host's load average was about 100, and these tests depend on timing; one of them runs before any watcher starts. I didn't confirm they also fail on the base commit. shellcheck reports nothing in the test file beyond info notes about files it didn't follow, and one existing warning in `fm-watch.sh` at line 2598, not from this change. I deleted the leftover temporary runner `tests/.tmp-one.sh`. Nothing is committed or pushed * fix(bin): re-read the watch lock holder after the steal check The steal-gap guard read the lock pid, then the steal mutex. A successor that removed and relinked the lock between those reads let the old watcher see no pid and then no steal mutex, so it exited as "state directory was torn down" instead of standing down. Upstream CI hit this in tests/fm-watcher-lock.test.sh (test_watch_replaces_live_holder_with_legacy_local_time_identity). The teardown branch now re-reads the pid after the steal mutex. The steal mutex is held for the whole remove-and-relink, so a steal that finished between the first two reads has already relinked the lock by the third read, and the self-eviction below stands the watcher down. Verification: - Drove: the takeover test (real bin/fm-watch.sh processes) 48 times, six at a time, with and without this change, plus the whole tests/fm-watcher-lock.test.sh file. - Observed: the interleaving is too narrow to hit locally (48 of 48 passed either way); the whole file passes with this change. The failing CI run shows the old watcher's exit 1 teardown message. - Re-check: Behavior portable serial 6 on this PR's CI. * no-mistakes(review): Scope fm_pid_identity residual comment to the watch lock * no-mistakes(review): Align fm_pid_identity residual comment with autoarm reclaim behaviour * no-mistakes(document): Note watch-lock identity mismatch skips lock stale grace * fix(bin): tell a released watch lock from a torn-down state directory A watcher replaced by a successor still exited 1 as "state directory was torn down" on upstream CI. The successor that reclaims the lock delivers its recovery wake and exits, releasing the lock, and on a fast runner that happens before the old watcher's next poll. The old watcher then found no lock holder and no steal in progress, which the teardown check read as a recreated state directory. The watcher now holds its state directory open (fd 6) from startup, and a lock with no holder is a teardown only when the directory at that path is no longer the one it holds; otherwise the lock was released by a successor and the self-eviction stands the watcher down with exit 0. The held descriptor keeps the inode from being reused by a recreated directory. A host that cannot open a directory keeps the old reading. This replaces the steal-marker check and pid re-read, which only covered a successor still relinking the lock. Verification: - Drove: real bin/fm-watch.sh processes. A new regression, test_watch_replaced_holder_stands_down_after_successor_exits, pauses the old holder (SIGSTOP, only while it holds no other state lock) until the successor has delivered its wake and exited, then resumes it. Also a standalone run with the old watcher polling every 5s. - Observed: on the previous head the old watcher exits 1 "state directory was torn down (singleton lock removed)", the CI failure, every time; with this change it exits 0 by standing down. Both takeover tests passed 20 of 20 runs four at a time on one loaded CPU. The teardown tests in tests/fm-watch-arm.test.sh (state directory and home removed) still pass. - Re-check: tests/fm-watcher-lock.test.sh and Behavior portable serial 6. * no-mistakes(test): Hold the watcher's state directory open before claiming the lock * no-mistakes(document): Docs already match watch-lock identity reclaim change --------- Co-authored-by: mangocabs2025 <250559406+mangocabs2025@users.noreply.github.com>
…nguid#7911) * Fix non-GNU timeout fallback * no-mistakes(review): Assert INT reaches watcher in non-GNU timeout fallback test * no-mistakes(document): Document GNU-only timeout selection in watch checkpoint * no-mistakes(ci): Greptile flagged that only the non-GNU-timeout-to-Perl path had an isolated test. I added two isolated tests in tests/fm-watch-checkpoint.test.sh, as you asked. The production code is unchanged. The rule being tested: run_bounded must run GNU `timeout` first, then GNU `gtimeout`, then Perl. Whichever GNU command it runs must receive `--signal=INT --kill-after=<bound>s <seconds> <watcher>`. What changed: - **Shared setup:** a new helper, `make_isolated_checkpoint`, copies the checkpoint into a temporary root. It also builds a PATH that holds only the named host tools, so the stubs decide which timeout is found. The existing Perl-fallback test now uses this helper too. Its assertions are the same. - **Stubs:** a non-GNU stub reports `uutils` for `--version` and exits 137 if it is ever used. A GNU stub reports `GNU coreutils` and writes its own name and arguments to `$FM_HOME/timeout-calls`, then exits 124. - **`test_gnu_timeout_runs_the_watcher_bound`:** GNU `timeout` and GNU `gtimeout` are both on PATH. The test asserts exit 124, the quiet checkpoint line, and exactly one call: `timeout --signal=INT --kill-after=5s 1 <root>/bin/fm-watch.sh`. - **`test_non_gnu_timeout_uses_gnu_gtimeout`:** a non-GNU `timeout` is next to a GNU `gtimeout`. The test asserts the same exact call, made by `gtimeout`. - In both new tests, Perl is not on PATH. So if the code picked Perl by mistake, the run would fail with 127. How I checked it: - The full test file passes: all 14 tests. - I broke the code on purpose twice. When I swapped the order to try `gtimeout` first, the GNU timeout test failed. When the code always picked Perl, the run exited 127 instead of 124. I then put the production file back; `git status` shows only the test file changed. - `shellcheck -x` passes on the test file. This is a change to tests only, so there is nothing a user would see to check by hand
* fix: recover ownerless extension readiness and announce cleanup refusals * no-mistakes(review): Reset cleanup failure episodes and classify failure wakes * no-mistakes(document): Document ownerless readiness recovery and cleanup failure wakes * no-mistakes(document): Clarify cleanup failure documentation ownership --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
kunchenguid#7874) * Keep malformed remote reply captures from wedging * no-mistakes(document): Fix garbled remote reply source failure comment * no-mistakes(document): Document remote reply failure handling --------- Co-authored-by: Pitch Perfekt Collective <pitchperfekt@Pitchs-Mac-Studio.local>
…n PR text (kunchenguid#7937) * feat(bin): per-project pr=manual registry token for hand-written PR text A no-mistakes ship on a project where an outside maintainer reviews the PR let the pipeline's pr step rewrite the hand-written title and body on every validated push. The registry now records pr=manual, resolved by fm-project-mode.sh --pr and carried by fm-brief.sh --pr, fm-spawn.sh and fm-promote.sh like forge=. The generated brief skips pr and ci, and the worker writes and publishes the PR text itself. * no-mistakes(review): Drop pr_text key; fix manual-PR checks wait and base * no-mistakes(review): Remove dead PR_TEXT and timeout example * no-mistakes(document): Fix README pr=manual sentence grammar * test(bin): restore the displaced placeholder comment and rename the spawn agreement test The comment for the leftover-placeholder test sat above the new pr=manual tests. The spawn test said it recorded the PR text, but it only checks that a launch happens.
* fix(teardown): detect untracked work regardless of Git status config * test(watcher): avoid blocking FIFO contention handshakes * no-mistakes(document): Document hidden-untracked teardown safety --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
* fix: prefer current run over stale overview row * fix: preserve newest run precedence over stale status * no-mistakes(document): Document live-run precedence in crew-state attribution * fix: use ledger order for same-minute run precedence * test: cover both overview orders for same-minute run attribution Document and pin newer-first (recorded AXI) and historical-first overview orders. Same-minute ledger timestamps use row order as the tie-break so an older live bare status cannot hide a newer completed run. * no-mistakes(review): Treat failed as overridable terminal overview status * no-mistakes(review): Fix equal-head live run precedence with identity ordering * no-mistakes(document): Updated run identity ordering documentation * no-mistakes(lint): Fix ShellCheck export warning * no-mistakes(review): Prove same-head ordering or report unknown * no-mistakes(review): Guard failed selected-run reads and drop dead same-head ordering * no-mistakes(ci): I fixed both Greptile findings (ci-1, ci-2) in bin/fm-crew-state.sh. The change is in the working tree. I did not commit it, push it, or reply to the Greptile threads, because those steps belong to the outer pipeline. ci-1. Invariant: a live run from bare `axi status` may replace a terminal overview selection only when it is proven newer than every other run on the same branch. Previously the code compared it only against the selected run. Now crew-state reads each other candidate with `axi status --run <id>` and compares the live run against each one: - If another candidate is also live, it reports `unknown`, "competing live runs". This keeps the competing-live-runs check. - If two runs share a head and a ULID timestamp, it reports `unknown`, "same-head run ordering unverified", as before. - It promotes the live run only when it is proven newer than all other candidates. - If the live run is newer than the selected run but not proven newer than some other candidate, it reports `unknown`, "run ordering unverified", and names every run id. This means neither the stale selected run nor the live run can hide a newer failed run. This branch is the one judgement call: without it, the stale "done" row A would still hide C's failure. - In all other cases the overview's selection stands. The lib helper is now a pairwise compare called `fm_nm_run_is_newer_than` (it was `fm_nm_run_is_newer_than_selected`). The selected run's detail from the loop is reused when the read succeeded, as in the round-1 fix. I updated the sentence in docs/architecture.md to match. ci-2. Invariant: the `no-mistakes runs` ledger is read only to order runs with different heads. It is now fetched lazily, at most once, and only when a candidate's head differs from the live run's head. Same-head ULID comparisons never call it. Regression test: `test_live_run_newer_than_selection_does_not_hide_newer_failed_run` covers A (stale completed, listed first), C (newer failed) and B (live, created between them). All three share one head and use ULID ids. It expects `state: unknown`, C's id in the output, and no `state: working`. Before the fix it fails: with the fixed code stashed it printed `state: working ... run: 01M1...` (run B). On the original base 3fbace9 the overview would select A, so the output would be `done`, which also fails the `unknown` check. I did not run that base case. Verification: `bash tests/fm-crew-state.test.sh` passes in full ("all fm-crew-state tests passed", the new test included). `shellcheck` on the three changed shell files reports only the existing info-level SC1091 "not following sourced file" notes. No end-to-end run against a live no-mistakes daemon was possible here. I checked the change with the crew-state test harness and its fake `no-mistakes` CLI --------- Co-authored-by: mehulbhagwani <mehulbhagwani@users.noreply.github.com> Co-authored-by: Pitch Perfekt Collective <pitchperfekt@Pitchs-Mac-Studio.local>
… full-ci label Outside pull requests run only the coverage guard and repo invariants until the full-ci label is added; lint and every test lane wait. A new Full suite check fails naming the full-ci label until it is added, and fails whenever a lane fails or is cancelled. The label starts the full suite, and later pushes run it again; main pushes, own branches and write-access authors run everything as before. Other labels run no job at all: their skipped checks take a 'Not run for label - ' prefix, so they cannot hide an earlier failure or a later pass. Close main pushes keep only the newest run; CONTRIBUTING.md explains the label, and the full-ci label now exists.
…atch resolution (kunchenguid#7664) * feat(bin): add opt-in systemone and openai providers to typed dispatch resolution Extend the FM_DISPATCH_PROVIDER/FM_DISPATCH_MODEL seam with a generic systemone provider (any System One-compatible host at FM_DISPATCH_BASE_URL, optional FM_DISPATCH_API_KEY) and an openai provider that maps the Choice request to OpenAI's Decisions API and its answer back. No provider set keeps the original off line; the never-send check runs on the endpoint and the final per-provider body. Live provider captures are recorded as pending. Refs kunchenguid#7643, builds on kunchenguid#7149. * docs(verification): record the live OpenAI Decisions capture for dispatch resolution Add a dated 2026-10-23 section for the openai provider: 25 hand-labeled synthetic briefs against gpt-6-luna, outcome and latency tables, the usage and header fields actually returned, a calibration note, and one verbatim request and response pair. The systemone live check stays pending. Refs kunchenguid#7643. * docs(verification): record the live Clef capture for the systemone provider Replace the pending systemone section with a dated 2026-10-23 run against Cloudflare Workers AI's @cf/cloudflare/clef, reached through a local test-only relay because Workers AI serves System One at /ai/run/<model> in a {result, success} envelope. Adds the outcome and latency tables, a calibration note compared with the gpt-6-luna run, and one verbatim request and envelope pair. A direct Workers AI option stays out of scope. Refs kunchenguid#7643.
…henguid#7941) * feat(agy): promote Antigravity CLI to full primary and secondmate harness Add tracked .agents/hooks.json, bin/fm-sessionstart-agy.sh, bin/fm-turnend-guard-agy.sh, PreToolUse --agy seatbelt flags, session-lock agy ancestry, autoarm supervision model, and --secondmate agy workspace trust pre-registration and spawn support. * no-mistakes(review): Fix agy Stop-hook park, hooks.json shape, and live coverage * no-mistakes(review): Unbound agy Stop park, raise ceiling, prove live deny * no-mistakes(review): Correct agy verification doc park-bound wording * no-mistakes(document): Document agy as verified primary and secondmate harness * no-mistakes(ci): I fixed all three selected findings, and every check I ran passed, including the live agy end-to-end run. **ci-1 (`tests/fm-agy-primary-live-e2e.test.sh`):** The invariant is that the live Stop-hook test passes only if a real watcher starts and its wake reaches a follow-up turn. The old test passed as soon as `.agy-park-owner` existed. I replaced the Stop-hook park block with the one the user specified. It waits until both `.agy-park-owner` and `.last-watcher-beat` exist, wakes the watcher with a `done:` status line, and asserts that the park owner was recorded, the watcher started, and `DONE` appears in the agy output. **ci-2 (agy away daemon):** The invariant is that `/afk` must not launch a separate away daemon on any primary whose home runs the supervision host. The agy Stop-hook guard already runs the host when it is enabled, but `fm_afk_launch_host_primary` left agy out. I added `agy` there, which is the one shared check all three callers use. I also added agy to: - the header comment in `bin/fm-afk-launch.sh`; - the three harness loops and the pass message in `tests/fm-afk-launch.test.sh`; - the primary lists and counts in `docs/supervision-host.md`, the matching lines in its engine-selection section, and a new Antigravity (`agy`) row in its Arm owners table; - the header comment in `bin/fm-supervision-instructions.sh`. **user-1 (`.agents/hooks.json`):** The invariant is that each hook command stays one script under Windows `cmd /c` and exits 0 when `sh` is missing. All 5 commands now use `sh -c "..." || cd .`. This does not change behaviour on Linux or macOS, because the `--agy` hooks report deny or continue as JSON on stdout and exit 0. The file keeps its live-verified `{"firstmate":{...}}` shape, matchers and 28800s Stop timeout. **Verification:** - `tests/fm-agy-primary.test.sh` passes, including running the SessionStart hook command from both `.agents/` and the repo root. - `tests/fm-afk-launch.test.sh` has no failures, and the new agy check passes. Its rex end-to-end cases were skipped because the rex CLI is not installed. - `tests/fm-supervision-instructions.test.sh` has no failures. - `bin/fm-lint.sh` and `bin/fm-doc-audience-check.sh` pass. - `FM_AGY_PRIMARY_LIVE=1 tests/fm-agy-primary-live-e2e.test.sh` printed all four `ok` lines and no failures, including "Stop hook parks on a real watcher and delivers its wake to a follow-up turn". The output went through `tail`, so the exit code shown is `tail`'s, not the test script's
…kunchenguid#7894) * feat(dispatch): add shared_quota home opt-out from quota-based ranking A home whose Claude and other provider usage draws on one combined AI budget has no meaningful per-provider quota rows, so spendPriority ranking and runway gates pick the wrong candidate. An optional top-level "shared_quota": true in config/crew-dispatch.json now opts the home out: the typed resolver makes no quota-axi call, applies no quota floor or runway gate, ranks nothing by spendPriority, and resolves the matched rule's use profiles in declared order, printing every candidate line with quota ranking off by home declaration. Firstmate's quota-array-dispatch intake honors the same declaration. Without the setting, behavior is unchanged. Non-boolean values are actionable configuration errors in both the resolver and bootstrap. * test(dispatch): cover shared-quota bypass of unmet rule and profile floors The shared_quota opt-out test picked a rule with no quota floor, so the separate rule-floor and profile-floor bypass paths stayed untested. Two cases now run against the same fixture: a rule whose model:fable floor (20%) exceeds the snapshot's 15%, and a profile whose all_models floor (50%) exceeds codex's 31%, each asserting the declared-order pick still wins with no quota-axi invocation. * no-mistakes(review): Narrow shared-quota skill opt-out to quota-only gates * no-mistakes(review): Document preferred_provider under shared_quota opt-out and test it * no-mistakes(document): Document shared_quota in schema example and verification inventory
…henguid#7958) * fix(bin): distinguish recycled reaper PIDs in tombstone cleanup * no-mistakes(ci): ci-2 fixed in tests/fm-watcher-lock.test.sh (test_lock_tombstone_tracks_reaper_start_time). Invariant: no fixture child may outlive a SIGKILLed runner without a bound. (1) The paused reaper's wait loop now also stops when FM_TEST_SUITE_PID is no longer alive or when SECONDS reaches FM_TEST_STUB_MAX_BLOCK_SECONDS. (2) The one sibling child in the same test, the unrelated `sleep 300`, now sleeps for FM_TEST_STUB_MAX_BLOCK_SECONDS so it uses the same bound. Verification, run serially: bash tests/fm-watcher-lock.test.sh. The new test passed on 4 of 4 runs. Red-before still holds: with the base bin/fm-wake-lib.sh from c9da552, the test fails with 'not ok - recycled reaper pid kept an abandoned tombstone alive'. Separately, the known stand-down flake ('the replaced watcher did not stand down on its own', watch-legacy-local-time-identity) failed on every run on this host and also fails on base c9da552 without this change, so the test file still exits 1; left unfixed as instructed. ci-1 (Full suite waiting on the full-ci maintainer label) was ignored per the user's decision
* feat: allow omp as a remote secondmate harness * test: cover omp on the remote secondmate gates * fix: retire a remote secondmate whose busy state left with its home * docs: record omp remote retirement evidence * review: Refresh pinned remote doctor bootstrap checksum
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
trillium
force-pushed
the
fix/spawn-refuse-live-sibling-worktree
branch
from
October 30, 2026 04:55
1ae1ba5 to
b129e04
Compare
A pooled worktree handed back by treehouse (or created by orca) can still be owned by a sibling task whose agent endpoint is alive. Launching a second worker into it tangles two tasks in one checkout. Before the isolation check, validate_spawn_worktree now scans this home's state/*.meta for another task that records the same worktree or home and whose endpoint is positively alive, and refuses the spawn. Only a positively alive endpoint vetoes, so a stale record never blocks a fresh slot, and the calling task's own record never counts against it (relaunch adopts it).
trillium
force-pushed
the
fix/spawn-refuse-live-sibling-worktree
branch
from
October 30, 2026 05:10
b129e04 to
28cf1ef
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The developer, acting through Firstmate operational and steering inbox messages, wanted the coding agent to fix PR kunchenguid#7823 in the firstmate repo. The bug: when fm-spawn.sh refuses a spawn because a live sibling task already holds the worktree, it exits with the Orca abort cleanup still armed, so it removes the sibling's Orca worktree. The fix had to disarm that cleanup on the live-sibling refusal path, keep the sibling's worktree in place, and add a regression test in tests/fm-spawn-worktree-ownership.test.sh that fails without the fix. The change then had to go through the no-mistakes pipeline and be pushed to update the PR. Decisions that needed the captain or Firstmate, such as accepting that live Orca validation was inconclusive because Orca isn't installed on the host, were escalated rather than answered by the agent. Host trust and config changes, like editing known_hosts or changing the fork remote, were also left for Firstmate to authorize. Follow-up PRs kunchenguid#7833 and kunchenguid#7829 were queued but not part of this change.
What Changed
bin/fm-spawn.shnow refuses to launch into a worktree (or home) that another task in the same home still records with a positively alive agent endpoint, and names the owning task in the error. A newspawn_worktree_sibling_ownerhelper scansstate/*.metafor the matches. Stale records don't block, and a relaunching task's own record doesn't count against it. The header docs describe the new refusal.ORCA_ABORT_CLEANUP=0before it exits. Without this, the abort trap would remove the sibling's Orca worktree (Orca removes with--force) and close its terminal.tests/fm-spawn-orca-worktree.test.sh. A fake Orca hands back a live sibling's existing worktree, and the test checks that the refusal names the owning task and leaves the worktree and terminal in place. Intests/fm-spawn-worktree-ownership.test.sh, the fake tmux now records window names fromnew-windowand reports them fromlist-windows, so a task can see its own endpoint as alive.Risk Assessment
✅ Low: This is a small, well-bounded fix: a positively-alive-only sibling-owner veto, plus disarming ORCA_ABORT_CLEANUP on that refusal so the trap no longer runs
orca worktree rm/terminal close against the sibling's resources; the Orca regression test drives the real abort path and fails without the one-line disarm.Testing
Ran the focused ownership test file and the Orca tests selected on their own. They pass with the fix. Deleting just the new
ORCA_ABORT_CLEANUP=0line makes the Orca regression test fail, as it should. Then I drove the real fm-spawn.sh in a disposable lab home on a private tmux server. With a real claude sibling alive in the target worktree, the spawn was refused: it namedtask sib-task, and the sibling's window, its claude process and its git worktree were all left in place. Once the sibling window was gone (a stale record), the spawn went ahead. A spawn into a fresh real treehouse slot was also accepted. Orca-backed spawns could only be checked with the repo's fake-orca tests, because the Orca CLI isn't on this host. One cleanup note: the fresh-slot run used real treehouse, which created a pool under ~/.treehouse/proj-e9142a. I returned and destroyed that slot and removed the directory. The lab was torn down, and no changes or temporary files remain in the worktree. This is a CLI change with no UI, so there are no screenshots; the evidence is CLI transcripts.orcaCLI is not on PATH and there's no copy in the repo, so a real Orca backend can't be run here. The real fm-spawn.sh was run only against the test's fake orca (orca-selected-tests.log), which…Evidence: Live tmux: spawn into a live sibling's worktree is refused; sibling window and worktree kept
Source: Live tmux: spawn into a live sibling's worktree is refused; sibling window and worktree kept
Evidence: Live tmux: a stale sibling record does not block the spawn
Source: Live tmux: a stale sibling record does not block the spawn
Evidence: Live tmux: a fresh real treehouse slot is accepted
Source: Live tmux: a fresh real treehouse slot is accepted
Evidence: Orca regression test fails with the fix line removed
Source: Orca regression test fails with the fix line removed
not ok - the refusal removed the live sibling's Orca worktreeEvidence: Temporary revert used for the adversarial run
Source: Temporary revert used for the adversarial run
Evidence: Selected Orca tests with the fix
Source: Selected Orca tests with the fix
Evidence: Ownership test file output
Source: Ownership test file output
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
tests/fm-spawn-worktree-ownership.test.sh- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
orcaCLI is not on PATH and there's no copy in the repo, so a real Orca backend can't be run here. The real fm-spawn.sh was run only against the test's fake orca (orca-selected-tests.log), which…bash tests/fm-spawn-worktree-ownership.test.sh(live sibling refused, stale record not blocking, fresh worktree accepted, the task's own worktree accepted)Selected Orca tests run on their own:test_orca_live_sibling_worktree_refusal_keeps_the_sibling_checkout,test_orca_fresh_spawn_enters_the_worktree_it_created,test_orca_relaunch_is_refused_before_the_worktree_carveout_could_runAdversarial: deleted only theORCA_ABORT_CLEANUP=0line at bin/fm-spawn.sh:4352, reran the Orca regression test (it fails: 'the refusal removed the live sibling's Orca worktree'), then restored the fileLive lab (lab home frombin/fm-lab-home.sh create, privatetmux -L fm-labsocket): realbin/fm-spawn.sh new-task-a1 <lab-proj>with a realclaudesibling window recorded in state/sib-task.meta, and a lab-local treehouse shim that drops the pane into the sibling's worktreeLive lab: killed the sibling window so its record is stale, then reran the real spawn into the same worktreeLive lab: an earlier real spawn through real treehouse got a fresh slot and was accepted (the slot was then returned and destroyed with treehouse, and its pool directory removed)Teardown:tmux -L fm-lab kill-server,rm -rf $LAB, checked that no stray treehouse pool and no changes in the worktree remain✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.