Repository navigation
fix(bin): pin markdown backend in backlog handoff and receive - #7047
Merged
kunchenguid merged 3 commits intoOct 11, 2026
Merged
Conversation
Fixture homes now write the tracked markdown defaults through a fm_test_tasks_toml helper instead of copying the checkout's .tasks.toml, which an operator may override with a beads backend at a live backlog. The backlog handoff and receive moves pin --backend markdown, because they already name their markdown files and otherwise resolve the backend from the caller's working directory. The atomicity suite runs from its scratch root so its direct tasks-axi fixture calls cannot read the checkout config.
Owner
|
Speaking as Kun's firstmate: this is merged. Thank you @RooseveltAdvisors — really appreciate you taking the time on this. |
trillium
pushed a commit
to trillium/firstmate
that referenced
this pull request
Oct 12, 2026
…nguid#7047) * fix(tests): keep backlog suites hermetic against a local .tasks.toml Fixture homes now write the tracked markdown defaults through a fm_test_tasks_toml helper instead of copying the checkout's .tasks.toml, which an operator may override with a beads backend at a live backlog. The backlog handoff and receive moves pin --backend markdown, because they already name their markdown files and otherwise resolve the backend from the caller's working directory. The atomicity suite runs from its scratch root so its direct tasks-axi fixture calls cannot read the checkout config. * no-mistakes(review): Add decoy-cwd .tasks.toml regressions for handoff and receive * no-mistakes(lint): Reuse sha256_file helper to fix SC2015 in remote handoff test
pc-agents-lstutzman Bot
added a commit
to lstutzman/firstmate
that referenced
this pull request
Oct 13, 2026
* fix: avoid argument-size failures in fleet contribution snapshots (kunchenguid#6830) * fix(bin): stage contribution-input JSON via temp files The contribution-input branch passed the whole canonical backlog document to jq as a single --argjson argv argument. A large fleet's backlog JSON exceeds Linux MAX_ARG_STRLEN (128 KiB), so jq fails with E2BIG (Argument list too long) and the contribution-input snapshot emits empty output, breaking the bearings contribution path. Write both the backlog and task documents to a temp directory and hand them to jq with --slurpfile, matching how the full snapshot path already stages the same pair. Output is unchanged for any backlog that fits in argv. * no-mistakes(document): Correct contribution cleanup trap comment * fix(bin): arm the busy contract on secondmate launches (kunchenguid#5342) * fix(bin): arm the busy contract on secondmate launches A tmux-backed secondmate's active-turn gate can only see a busy verdict from the parent home's record, and those launches never armed it. * no-mistakes(review): Reopen secondmate busy on blocked Claude Stop; drop muse guard * no-mistakes(review): Make Claude secondmate Stop guard the sole Stop-state writer * no-mistakes(review): Record live rewake UserPromptSubmit evidence and add regression * docs: record Claude secondmate Stop guard as busy writer * no-mistakes(review): Keep Stop idle hook for older secondmate guards * no-mistakes(document): Correct stale omp secondmate -e rationale in supervision verification * no-mistakes(document): Fix skill-relative turnend-guard link in Claude harness reference * no-mistakes(ci): Root cause: in shard 9, the pure-contract-unit family ran two scripts, tests/fm-rovo-harness.test.sh (14712 ms) and tests/fm-omp-harness.test.sh (10335 ms). Those two durations add up exactly to the failing family's 25047 ms. The failing one was tests/fm-omp-harness.test.sh. Its test_secondmate_launch_relies_on_discovery still asserted the old contract: that an omp secondmate launch names no -e and gets no state/<id>.omp-ext.ts. This PR (issue kunchenguid#4268) deliberately changed that. bin/fm-spawn.sh now arms the busy contract for omp secondmates, so their launch names `-e state/<id>.omp-ext.ts` and writes that extension. The primary extensions are still left to auto-discovery. The harness reference docs were updated to match, but the test was not. The fix is test-only and touches just tests/fm-omp-harness.test.sh. The test now asserts that the secondmate launch names exactly one -e and that it points at the parent-home state/sm.omp-ext.ts. Because only one -e is allowed, the tracked .omp/extensions files are not named a second time, which would load them twice. The test also asserts that the extension file exists, and the header comment for contract 3 was updated. The posture overlay, --cwd, marker, and supervision-model assertions are unchanged. Verification: the host's load average was about 140 on 8 cores, and the full script timed out locally after 590 s having finished only its first 2 tests. So I ran the one affected test function on its own through the real fm-spawn path, and it passed (`ok - fm-spawn: a real omp secondmate launch loads only the busy -e and auto-discovers its primary extensions`). The remaining omp tests were not re-run locally. shellcheck on the file reports only existing info-level notices. The change is left uncommitted for the outer executor * test: keep the secondmate busy stall gate visible after main's liveness probe The merged watcher treats a window-name inventory miss as a missing endpoint and exits a later checkpoint on the previous quiet run's recovery wake. The fake pane now answers both list-windows formats, and each observation starts without a leftover downtime marker, so a busy mate still defers the stall and an idle mate still surfaces it. * no-mistakes(review): Align fm-spawn secondmate comments with omp -e and devin refusal * no-mistakes(document): Document secondmate busy extensions in configuration reference * no-mistakes(test): Make live busy-contract guard clean up read-only lab dirs * fix(bin): pass Claude secondmate busy hooks through --settings Rewriting a persistent mate home's settings.local.json drops the captain's saved permissions. The hooks now live in a parent-state file Claude Code loads for that session only. * no-mistakes(review): skip busy arm for raw secondmate launch commands * no-mistakes(review): retire earlier busy arm on raw secondmate respawn * no-mistakes(review): retire Stop pointer on relaunch, load --settings in live guard * no-mistakes(document): Document secondmate Claude settings file and raw-launch exception * no-mistakes(ci): The check "Behavior portable serial 9" failed because of this PR, and I fixed it with a test-only change; `tests/fm-spawn-dispatch-profile.test.sh` now passes locally in full. Cause: the one failing test was "claude secondmate launch settings JSON does not disable Claude attribution" in `tests/fm-spawn-dispatch-profile.test.sh`. This PR makes a claude secondmate launch pass `--settings` as a file path (`state/<id>.claude-settings.json`) instead of inline JSON. The test helper `claude_settings_json_arg` handed that path string to jq as if it were JSON, so jq reported a parse error. The product behavior is correct: the file does carry `feedbackDrafts` and the attribution-off policy. Invariant: a test that checks the Claude launch settings must read the settings Claude would load, whichever form `--settings` takes (inline JSON or a file path). The only place this must hold is `claude_settings_json_arg`, the single reader of the `--settings` value in the tests. Its two callers, `assert_attribution_policy` and `assert_attribution_policy_absent`, both go through it. Fix: `claude_settings_json_arg` now prints the value when it starts with `{` and prints the file contents otherwise. No product code changed. Verification: I ran `bash tests/fm-spawn-dispatch-profile.test.sh` in the worktree. It ended with "all fm-spawn-dispatch-profile tests passed", including the previously failing secondmate case and the crewmate and keep-ai-trailers cases that use the inline form. I did not run the other tests of shard 9; they passed in the CI run and this change touches only this test file. The change is uncommitted in the worktree (one file modified) * test: guard fleet snapshot contribution input against argument-size failures (kunchenguid#7036) The large-backlog regression already proved the json and secondmate home-summary modes stay within exec limits. It now also runs the contribution-input mode on the same 1200-record backlog, which is the path kunchenguid#6830 moved off argv. With that fix reverted the test fails with the field error jq Argument list too long at the contribution-input call. * fix(bin): scrub nested repos from scratch copies before treehouse return (kunchenguid#5843) * fix(teardown): scrub nested repos before return Treehouse return uses git clean -fd, which leaves a nested git repository in the pool slot. Scout and forced teardown run git clean -ffdq first. A return that stays dirty names the lost slot instead of reporting a clean return. * no-mistakes(review): Report lost pool slots early and scrub forced child copies * no-mistakes(review): Drop misleading no-rerun clause from lost-slot message * no-mistakes(document): Note failed status read in teardown dirty-return header * no-mistakes(ci): I fixed ci-1 and ci-2 in bin/fm-teardown.sh, and I added tests in tests/fm-teardown.test.sh. Invariant (ci-1): a lost-slot finding must only warn. It must never change the teardown exit status. Two places must obey this: the final status block, and the remote success path that a forced remote retirement uses. - The final block no longer runs `exit 1`. When a slot is lost, teardown writes a lost-slot line to stderr instead of the clean "teardown X complete" line. Then backlog_refresh_reminder runs and teardown exits with its normal status. - Before this change, remote_secondmate_teardown discarded the remote output when the remote run succeeded. Now it sends that output to stderr. Without this, the remote lost-slot warning would not reach the operator once the remote side exits 0. - I updated the header comment to match. Invariant (ci-2): after a return that succeeds, teardown must verify every returned copy, or it must warn that it could not. This invariant is enforced inside teardown_note_dirty_return, which both return sites call (the main WT return and the forced-secondmate child return). Before, a copy that was missing or not a readable git worktree caused a silent `return 0`. Now it sets the lost flag and gives the same warning, with the text "cannot verify: the copy is missing or not a readable git worktree after return". Both call sites run only for a copy that existed before the return. The exit status does not change. Tests: - The dirty-return test now expects exit 0 and the warning. - New test (z3d): a forced secondmate teardown whose child return leaves residue. This is the same command that `retire --force` runs on the remote host. The test expects exit 0 and the warning, and it checks that the task record and the home are removed. - New test (z3e): a return that removes the copy's .git. The test expects exit 0, the "cannot verify" warning that names the copy, and no clean completion line. Verification: - I ran the new and updated tests against the old script. Three failed as expected (exit 1, or no warning). With the fix, all of them pass. - The full tests/fm-teardown.test.sh suite passes: 117 ok, 0 not ok. - shellcheck shows no new warnings. - tests/fm-remote-secondmate-lifecycle-e2e.test.sh is still running. It covers the remote success path that now relays output. Up to now it has no failures, but it has not finished, so I cannot report a final result for it * no-mistakes(ci): I fixed ci-1 and ci-2 as one exit contract, as the user asked. The changes are in bin/fm-teardown.sh, tests/fm-teardown.test.sh, and tests/fm-remote-secondmate-lifecycle-e2e.test.sh. Invariant (ci-1): when a returned slot is dirty or cannot be verified, teardown must finish every cleanup step and then exit with exit code 4. Exit 4 is used for nothing else. It must hold in two places: the final status of a local teardown, and the remote secondmate retirement caller. - Local: the existing lost-slot flag now leads to `exit 4` (TEARDOWN_LOST_SLOT_EXIT). This exit comes after the task record is removed, the warning is printed, and backlog_refresh_reminder runs. A clean return still exits 0. Refusals and errors exit 1, and an invalid request exits 2. The header documents all of these codes. Exit 3 is only an internal "not remote" return. The ssh transport uses 255. - Remote caller: remote_secondmate_teardown treats a remote exit 4 as "retired with a lost-slot warning". It does the full local retirement (route, registry line, reply source, records, summary refresh), prints the normal completion line on stdout, and then returns 4. The remote control script runs fm-teardown.sh as its last command, and fm-on.sh execs ssh, so the remote 4 reaches the local side unchanged. Invariant (ci-2): remote output goes to local stderr only when it is a lost-slot warning. - After a clean remote retirement (exit 0), the caller no longer prints the captured remote output. - After exit 4, the caller forwards only the lost-slot warning header and the remaining-path lines. To make those lines easy to find, teardown_note_dirty_return now indents each remaining path by two spaces. Tests: - The dirty-return (z3), forced-secondmate dirty-child (z3d), and copy-removed (z3e) tests now expect exit 4 after cleanup. The clean scout return (z1) still expects 0. - The final remote retirement in the e2e test now uses a fake-ssh mode, retire-lost-slot. In this mode the real remote retire runs, and then the fake reports a lost child slot with exit 4. The test checks the following: exit 4; the remote home, parent metadata, and route are removed; stdout has "teardown ios complete (remote"; stderr has the warning and the remaining path; and stderr does not contain the remote completion text. The remote side producing exit 4 is covered by z3d. Verification: - tests/fm-teardown.test.sh: 117 ok, 0 not ok. - tests/fm-remote-secondmate-lifecycle-e2e.test.sh: ALL TESTS PASSED. - shellcheck shows the same warning count as before on all three files. - Against the previous script, both suites fail as expected. In the local suite, dirty-return expected 4 but got 0. In the e2e test, the stdout local-completion check fails * no-mistakes(ci): Both CI failures came from the lost-slot check that this PR added. Shard 3: tests/fm-secondmate-safety.test.sh "force teardown failed to discard child work". Shard 7: tests/fm-teardown-endpoint-safety.test.sh exited 4. The check ran after the return of every copy. Treehouse return removes a copy that is not a pool slot, so the check reported "cannot verify" and teardown exited 4. Before the fix, both failures happened on my machine. Invariant: only a Treehouse pool slot can be lost by the pool, so the check runs only for a copy that was a pool slot before the return. Fix in bin/fm-teardown.sh: both return sites (the main WT return and the forced-secondmate child return) now run fm_treehouse_pool_slot before the return. They call teardown_note_dirty_return only when the copy was a pool slot. teardown_note_dirty_return lost its pool-slot branch and its unused proj argument; the slot label still comes from the path of the copy. Test fix: in tests/fm-teardown.test.sh, the z3d child copy was not a pool slot. It is now a real pool slot (pool/6/repo plus treehouse-state.json), and the test still expects exit 4 and the warning. Verification: endpoint-safety 27 ok, exit 0. secondmate-safety 79 ok, exit 0. fm-teardown 117 ok, 0 not ok. remote-secondmate-lifecycle-e2e ALL TESTS PASSED. shellcheck count is unchanged (17) --------- Co-authored-by: Nova Agent B <novaagentb@gmail.com> * fix(bin): pin markdown backend in backlog handoff and receive (kunchenguid#7047) * fix(tests): keep backlog suites hermetic against a local .tasks.toml Fixture homes now write the tracked markdown defaults through a fm_test_tasks_toml helper instead of copying the checkout's .tasks.toml, which an operator may override with a beads backend at a live backlog. The backlog handoff and receive moves pin --backend markdown, because they already name their markdown files and otherwise resolve the backend from the caller's working directory. The atomicity suite runs from its scratch root so its direct tasks-axi fixture calls cannot read the checkout config. * no-mistakes(review): Add decoy-cwd .tasks.toml regressions for handoff and receive * no-mistakes(lint): Reuse sha256_file helper to fix SC2015 in remote handoff test * fix(bin): parse whole project names in home seed registry queries (kunchenguid#7076) * fix(bin): parse whole project name in home seed registry queries Parse the full project name after the dash in fm-home-seed.sh registry lookups. This matches the round-3 bootstrap mirror check parser so spaced names resolve. Cover both the project-less seed refusal guard and registry line lookups in tests. * no-mistakes(review): Match registry rows by name prefix plus separator * no-mistakes(document): Note shared registry row-match rule in fm-home-seed * fix: preserve announced recovery across Codex checkpoints (kunchenguid#4490) * fix(codex): preserve announced recovery across checkpoints * no-mistakes(review): Make announced-recovery checkpoint test reproduce the regression * no-mistakes(review): Separate 2026-10-07 checkpoint fixture verification from original run * no-mistakes(test): Defer TERM exit to clean up quiet checkpoint watcher locks * no-mistakes(document): Update checkpoint recovery and signal documentation * no-mistakes(ci): Updated both lock-acquire retry loops to honor the watcher’s TERM stop status, so held-marker cleanup exits while retaining stale lock evidence. Set the --stop test’s watcher poll to 1 second to fit its 5-second stop bound. Syntax checks and the full watch-arm suite passed; the targeted held-marker test passed during the triage suite, which timed out after 600 seconds before completing. Did not change or attempt to fix fm-supervision-host.test.sh * no-mistakes(ci): Removed the cleanup-bound test that incorrectly expected a TERM-stopped watcher to retry a contended lock acquisition. TERM cleanup intentionally stops after the failed attempt; the remaining held-lock test verifies it exits and retains stale lock evidence. The two relevant TERM tests passed, as did shell syntax and diff checks * Revert "no-mistakes(ci): Updated both lock-acquire retry loops to honor the watcher’s TERM stop status, so held-marker cleanup exits while retaining stale lock evidence. Set the --stop test’s watcher poll to 1 second to fit its 5-second stop bound. Syntax checks and the full watch-arm suite passed; the targeted held-marker test passed during the triage suite, which timed out after 600 seconds before completing. Did not change or attempt to fix fm-supervision-host.test.sh" This reverts commit 44ce29b. * Revert "no-mistakes(ci): Removed the cleanup-bound test that incorrectly expected a TERM-stopped watcher to retry a contended lock acquisition. TERM cleanup intentionally stops after the failed attempt; the remaining held-lock test verifies it exits and retains stale lock evidence. The two relevant TERM tests passed, as did shell syntax and diff checks" This reverts commit e1e3706. * Revert "no-mistakes(test): Defer TERM exit to clean up quiet checkpoint watcher locks" This reverts commit 9877bf9. * fix: let checkpoint watcher clean up before timeout * no-mistakes(review): Scope checkpoint timeout escalation and restore signal docs * no-mistakes(review): Fix Perl checkpoint timeout escalation * no-mistakes(document): Clarify checkpoint recovery documentation * no-mistakes(document): Document isolated checkpoint regression command * no-mistakes(ci): Tied direct-checkpoint SIGKILL escalation to the effective cleanup bound, preserving the five-second minimum and leaving the supervision-host path unchanged. The targeted checkpoint suite, shell syntax checks, ShellCheck, and diff checks pass. CI was not manually rerun --------- Co-authored-by: jopo <jopo@JP-M2mini.local> * feat(bin): support Forgejo/Gitea as a forge via the tea CLI (kunchenguid#4395) * feat(bin): add Forgejo (tea) as a third forge alongside GitHub and GitLab Extends the existing provider-tagged PR/MR dispatch in fm-pr-lib.sh, fm-pr-poll.sh, fm-pr-check.sh, and fm-pr-merge.sh with a forgejo arm, following the same pattern already established for GitLab. tea addresses a repository by owner/repo slug only and resolves the host through a named "tea login" rather than a URL, so the login is matched against the PR's host fresh at arm and merge time. tea's own merge subcommand exposes no head-binding flag, so the merge itself calls Forgejo/Gitea's REST API directly via "tea api" with head_commit_id, verified against a real disposable instance to behave like --match-head-commit/--sha; that same verification found that "tea api" reports an HTTP-level failure with exit status 0, so a merge is confirmed only by reading the pull request back afterward, never by trusting the command's own exit code. Adds forgejo as a value of the registry's existing forge= binding (the closed set becomes none|gerrit|forgejo, with its refusals unchanged), so a generated brief and its direct-PR Definition of done name tea before any PR exists and bin/fm-spawn.sh's forge agreement and bin/fm-promote.sh carry it like gerrit. Documents the new value in docs/configuration.md, docs/architecture.md, and a new maintainer-verification doc mirroring docs/gitlab-merge-watch.md. * no-mistakes(review): Redact private Forgejo host/IP from evidence doc * no-mistakes(review): Fix doc's tea-login JSON shape; dedupe host validators * no-mistakes(document): Add Forgejo to README doc index and scripts.md forge list * no-mistakes(lint): {"summary": "fix(bin): drop unused FORGEJO_LOGIN capture in fm-pr-check.sh"} * fix(bin): refuse --allow-red on Forgejo instead of dropping it silently The attended --allow-red waiver landed on main while the Forgejo arm was being written, so its applicability guard only ever classified GitHub and GitLab. On a Forgejo pull request the flag parsed, and then nothing read it: forgejo_verify_mergeable requires the head's combined commit status to be exactly "success", with no per-check name to match a waiver against. The merge still refused, so no red Forgejo merge was ever possible, but it refused without saying the waiver had been ignored. Extend the existing GitLab guard to cover Forgejo for the same reason GitLab has it: both collapse their checks into a single head verdict, so the green requirement is absolute and a named waiver cannot apply. * fix(bin): poll a Forgejo pull request by number through tea api The merge watch listed the 50 most recent pull requests and filtered to the watched index, so in a repository with more newer pull requests a merged watch never fired. Read the one pull request through tea api instead and wake only on a boolean merged field of true, since tea api exits 0 on HTTP errors. Share the tea login match as fm_pr_forgejo_login for every sourcing script and add fm_pr_forgejo_read_record for the state, merged flag, and head. Arming now also requires jq, which the poll needs to read the record. * fix(bin): gate a Forgejo merge on every status page and on draft state The combined-status endpoint computes its state from one page of contexts, so a failing context past the first page could leave the head reading green. Read every page until an empty one and require each to be success. Also refuse a pull request the API reports as a draft. * fix(bin): prove a Forgejo task landed from its merged pull request at teardown pr_is_merged read the recorded pr= only through gh, so a Forgejo task always fell back to the content check and refused landed work whenever that check was inconclusive, such as when later default-branch commits conflict with the merged tree. Read a recorded Forgejo URL through fm_pr_forgejo_read_record and apply the same containment check against its head. * feat(bin): read Forgejo pull requests in crew-state and review-diff fm-crew-state reports a terminal passed run's Forgejo pull request as open, closed, merged, or unknown through fm_pr_forgejo_read_record, and fm-review-diff resolves a /pulls/<n> URL to the refs/pull/<n>/head ref Forgejo serves. Document the by-number read, the paged status gate, and the draft gate, and mark what the evidence instance did not exercise. * fix(bin): parse tea logins as JSON and close the remaining Forgejo gaps The tea login match split each line of `tea login list --output json` on quotes and read field 4, which only works while tea pretty-prints one field per line. A login printed as one compact object put the login name where the url belonged, so arming, merging, and polling refused every Forgejo host. fm_pr_forgejo_login and the static poll's own copy now parse the list with jq, matching each login's url host with scheme, userinfo, port, and path stripped, and still refuse zero or several matches. --allow-missing is now refused on a Forgejo pull request the way --allow-red already is, instead of being parsed and ignored: the merge already requires the head's combined commit status to have succeeded. The forge=forgejo Definition of done read the draft state back with gh-axi pr view and cleared it with gh-axi pr ready although the PR is opened with tea; both publishing modes now read the draft state through tea api and clear Forgejo's work-in-progress title prefix with tea pulls edit. * no-mistakes(review): Retry Forgejo merge confirmation read before failing * no-mistakes(document): Document Forgejo draft gate and merge confirmation retry * fix(bin): merge Forgejo pull requests with the repository's default style When the caller named no merge method, a Forgejo merge always sent Do=merge, so a repository that allows only squash or rebase merges refused every default call. The style now comes from the repository's own default_merge_style, read live through the same tea login, and a merge is refused when that value cannot be read or is not a known style rather than replaced by one chosen here. A method the caller names still wins. --------- Co-authored-by: dna <dna@local> * fix(bin): make remote second-mate retirement complete and converge on rerun Retiring a remote second mate launched on a runtime that records a busy-state generation failed after the host had already closed the pane and removed the home: the host-local teardown keeps its state directory inside that home, and the busy-state retire helper refuses a missing state directory. The parent then kept the route, its task record, and the reply source. A rerun could not finish either. Sourcing bin/fm-wake-lib.sh creates $FM_HOME/state, so the retire command recreated an empty directory at the retired path and then refused it as an unseeded home, which made the already-retired answer unreachable. Teardown now skips the busy-state retirement when a second-mate retirement removed the home that held the state directory. Host-local retire treats a home path holding only empty directories as already retired and removes that tree with rmdir; a path holding any file or symlink without the home marker is still refused untouched. --------- Co-authored-by: Martin Kessler <kesslerio@users.noreply.github.com> Co-authored-by: Tiago <tiagop@hey.com> Co-authored-by: Jon Roosevelt <rooseveltadvisors@gmail.com> Co-authored-by: guanchengh-lgtm <guanchengh@gmail.com> Co-authored-by: Nova Agent B <novaagentb@gmail.com> Co-authored-by: John Poyser <13007391+jcpoyser@users.noreply.github.com> Co-authored-by: jopo <jopo@JP-M2mini.local> Co-authored-by: Mirco Santori <mirco.santori@gmail.com> Co-authored-by: dna <dna@local> Co-authored-by: Lee Stutzman <leestutzman@Lees-Mac-mini.local> Co-authored-by: fmtest <fmtest@example.invalid>
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
Fix test-suite hermeticity against a cwd .tasks.toml (bead fm-tasks-toml-test-hermeticity). An operator's primary firstmate checkout keeps a gitignored .tasks.toml override (a beads backend with absolute paths into the live backlog). Suites that copied $ROOT/.tasks.toml into fixture homes, and product tasks-axi calls that resolve the backend from the caller's working directory, then produced false RED results: fm-backlog-handoff ('no beads database found') and fm-secondmate-lifecycle-e2e ('handoff failed for in-scope items'). Acceptance: those previously-RED suites must pass from the primary-checkout condition, proven by running them with a decoy beads .tasks.toml in the checkout. Do not weaken tests to make them pass, make them hermetic. Decisions: a tests/lib.sh helper fm_test_tasks_toml writes the tracked markdown defaults instead of copying the checkout config (18 sites in 12 suites). The scope deliberately extends into bin/: fm-backlog-handoff.sh and fm-backlog-receive.sh already pass explicit markdown --file paths to tasks-axi rm/mv, so they now also pin --backend markdown, since otherwise a .tasks.toml in the caller's cwd redirects a markdown handoff to another backend (a real product bug, not only a test artifact). fm-backlog-atomicity.test.sh now cds into its scratch root because its fixture helpers call tasks-axi directly. fm-captain-hold-lifecycle has one pre-existing failure ('could not create the fixture row fm-marked-dual-row') that reproduces identically on the unmodified base and is out of scope.
What Changed
bin/fm-backlog-handoff.shandbin/fm-backlog-receive.shnow pass--backend markdownto everytasks-axi rm/mvcall. These calls already name explicit markdown--filepaths, so the backend is now fixed too. Before this change, a.tasks.tomlin the caller's working directory (for example, an operator's beads override) could send a markdown handoff or receipt to a different backend.fm_test_tasks_tomlhelper totests/lib.sh. It writes the tracked markdown defaults into fixture homes. Suites that used to copy the checkout's$ROOT/.tasks.tomlnow call this helper instead, so a gitignored local override no longer reaches their fixtures.fm-backlog-atomicity.test.shalsocds into its scratch root, because its fixture helpers calltasks-axidirectly.fm-backlog-handoff.test.sh) and a receipt (fm-remote-backlog-handoff.test.sh) from a working directory holding a decoy beads.tasks.toml. Each test checks that the items still move between the markdown backlogs.🤖 Generated with Claude Code
Risk Assessment
✅ Low: The bin/ change only pins
--backend markdownon tasks-axi calls that already name explicit markdown files. The test changes swap checkout-config copies for a fixed tracked-defaults helper. The fix-round regressions run the real handoff and receive from a cwd that holds a decoy beads .tasks.toml, and they check observable file moves, so they match the user's fix instructions.Testing
Recreated the operator primary-checkout condition by putting a decoy beads .tasks.toml in the worktree. Under that decoy, the two previously-RED suites and the remote-handoff and atomicity suites pass on the change. Both previously-RED suites fail on the base commit with the reported 'no beads database found'. Removing --backend markdown from either bin script makes the suites fail. All temporary state was reverted; logs are in the evidence dir.
Evidence: Decoy beads .tasks.toml used
Source: Decoy beads .tasks.toml used
Evidence: fm-backlog-handoff with decoy (pass)
Source: fm-backlog-handoff with decoy (pass)
Evidence: fm-secondmate-lifecycle-e2e with decoy (pass)
Source: fm-secondmate-lifecycle-e2e with decoy (pass)
Evidence: fm-remote-backlog-handoff with decoy (pass)
Source: fm-remote-backlog-handoff with decoy (pass)
Evidence: fm-backlog-atomicity with decoy (pass)
Source: fm-backlog-atomicity with decoy (pass)
Evidence: Base commit fm-backlog-handoff with decoy (RED reproduction)
Source: Base commit fm-backlog-handoff with decoy (RED reproduction)
Evidence: Base commit fm-secondmate-lifecycle-e2e with decoy (RED reproduction)
Source: Base commit fm-secondmate-lifecycle-e2e with decoy (RED reproduction)
Evidence: Handoff suite with --backend markdown removed (fails)
Source: Handoff suite with --backend markdown removed (fails)
Evidence: Remote suite with receive flag removed (fails)
Source: Remote suite with receive flag removed (fails)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/fm-backlog-handoff.sh:1075- The product fix in bin/ (pinning--backend markdownon the explicit-filetasks-axi mv/rmcalls in fm-backlog-handoff.sh:746, :830, :1075 and fm-backlog-receive.sh:83) has no regression test. Nothing puts a decoy beads.tasks.tomlin the caller's cwd and then checks that a markdown handoff or receive still moves rows between the named files. If someone drops the flag later, the suites still pass, because every fixture now writes a markdown.tasks.tomland the atomicity suite cds into a scratch root. The bug the intent describes then comes back without any test failing. Suggested fix: add one case to fm-backlog-handoff.test.sh, and optionally fm-backlog-receive, that runs the helper from a directory containingbackend = "beads"and asserts the item moved from the main backlog file to the destination file.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Wrote a decoy beads.tasks.toml(db=/nonexistent/fm-decoy/beads.db) into the worktree checkoutbash tests/fm-backlog-handoff.test.shwith decoy -> exit 0bash tests/fm-secondmate-lifecycle-e2e.test.shwith decoy -> exit 0bash tests/fm-remote-backlog-handoff.test.shwith decoy -> exit 0bash tests/fm-backlog-atomicity.test.shwith decoy -> exit 0Base commit 072a7da3 in a temporary git worktree, same decoy: fm-backlog-handoff and fm-secondmate-lifecycle-e2e -> exit 1 'no beads database found'Mutation: removed--backend markdownfrom bin/fm-backlog-handoff.sh + bin/fm-backlog-receive.sh -> fm-backlog-handoff exit 1, fm-remote-backlog-handoff exit 1Mutation: removed the flag only from bin/fm-backlog-receive.sh -> fm-remote-backlog-handoff exit 1 (fails at an earlier receipt case)Restored .tasks.toml and bin/ via git checkout; removed the temporary base worktree; git status clean✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.