Skip to content

fix(bin): pin markdown backend in backlog handoff and receive - #7047

Merged
kunchenguid merged 3 commits into
kunchenguid:mainfrom
RooseveltAdvisors:fm/tasks-toml-hermeticity
Oct 11, 2026
Merged

kunchenguid merged 3 commits into
kunchenguid:mainfrom
RooseveltAdvisors:fm/tasks-toml-hermeticity

Conversation

@RooseveltAdvisors

Copy link
Copy Markdown
Contributor

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.sh and bin/fm-backlog-receive.sh now pass --backend markdown to every tasks-axi rm/mv call. These calls already name explicit markdown --file paths, so the backend is now fixed too. Before this change, a .tasks.toml in the caller's working directory (for example, an operator's beads override) could send a markdown handoff or receipt to a different backend.
  • Added a fm_test_tasks_toml helper to tests/lib.sh. It writes the tracked markdown defaults into fixture homes. Suites that used to copy the checkout's $ROOT/.tasks.toml now call this helper instead, so a gitignored local override no longer reaches their fixtures. fm-backlog-atomicity.test.sh also cds into its scratch root, because its fixture helpers call tasks-axi directly.
  • Added regression tests that run a handoff (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 markdown on 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.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
With a decoy beads .tasks.toml in the checkout, fm-backlog-handoff and fm-secondmate-lifecycle-e2e pass ✅ pass live fm-backlog-handoff.decoy.log and fm-secondmate-lifecycle-e2e.decoy.log (both exit 0)
Remote handoff/receive and atomicity suites stay green under the same decoy checkout config ✅ pass live fm-remote-backlog-handoff.decoy.log, fm-backlog-atomicity.decoy.log (exit 0)
Base commit reproduces the reported RED under the decoy ('no beads database found') ✅ pass live fm-backlog-handoff.base-decoy.log, fm-secondmate-lifecycle-e2e.base-decoy.log (exit 1, beads error)
Adversarial: handoff from a cwd with a beads .tasks.toml fails once --backend markdown is removed from fm-backlog-handoff.sh ✅ pass live fm-backlog-handoff.flag-dropped.log (exit 1, 'tasks-axi mv failed; nothing was moved')
Adversarial: receipt fails once --backend markdown is removed from fm-backlog-receive.sh ✅ pass live fm-remote-backlog-handoff.receive-flag-dropped.log (exit 1)
Evidence: Decoy beads .tasks.toml used

Source: Decoy beads .tasks.toml used

backend = "beads"

[beads]
db = "/nonexistent/fm-decoy/beads.db"
Evidence: fm-backlog-handoff with decoy (pass)

Source: fm-backlog-handoff with decoy (pass)

ok - a routed handoff wakes once and a successful rerun stays idempotent
ok - a cwd .tasks.toml for another backend does not redirect a markdown handoff
ok - a failed receiver wake is loud and retries from an already-present handoff
ok - a failed advisory doorbell leaves the inbox delivery successful and idempotent
ok - concurrent reconciliation treats enqueue as delivery despite a failed advisory ring
tests/fm-backlog-handoff.test.sh: line 263: 568057 Killed                  FM_REAL_TASKS_AXI="$real_tasks" PATH="$fakebin:$PATH" FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design crash-item > "$TMP_ROOT/move-crash.out" 2>&1
ok - a post-move crash preserves wake intent for an idempotent retry
tests/fm-backlog-handoff.test.sh: line 339: 581332 Killed                  FM_REAL_TASKS_AXI="$real_tasks" PATH="$fakebin:$PATH" FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design pre-move-crash > "$TMP_ROOT/pre-move-crash.out" 2>&1
ok - a pre-move crash wakes only after retry makes the item durable
tests/fm-backlog-handoff.test.sh: line 414: 592962 Killed                  PATH="$fakebin:$PATH" FM_REAL_RM="$real_rm" FM_CONFIRM_CRASH_ONCE="$TMP_ROOT/confirm-crash.once" FM_CONFIRM_WAKE_MARKER="$home/state/.backlog-handoff-design.wake-pending" FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design confirm-crash > "$TMP_ROOT/confirm-crash.out" 2>&1
ok - a post-confirmation crash reconciles once without suppressing a later handoff wake
ok - an unresolved delivery attempt refuses an immediate duplicate wake
ok - concurrent local handoffs serialize each durable move with its wake
ok - local teardown waits for the routed move and receiver wake
ok - failed local home removal preserves its wake and a retry retires both
ok - body followed by another item moves intact with no source orphans
ok - body followed by section heading moves intact; section stays
ok - multi-paragraph body with internal blank lines moves whole and is idempotent
ok - body as last lines of the file moves intact
ok - EOF body before a seeded destination section keeps its boundary
ok - untouched EOF line preserves its original terminator
ok - body-carrying handoff is idempotent: re-run changes nothing
ok - noncanonical one-space and tab continuations refuse without changes
ok - indented ## Intent / ## Acceptance are body, not section boundaries
ok - registry home parses when summary has parentheses before (home: ...)
ok - registry entry without (home: ...) fails cleanly with has no home
ok - handoff reports a moved item whose public commitment still binds this home
ok - handoff says nothing about public commitments in a relay-free home
ALL TESTS PASSED
Evidence: fm-secondmate-lifecycle-e2e with decoy (pass)

Source: fm-secondmate-lifecycle-e2e with decoy (pass)

Note: switching to '98f789703f2186f7b01e4a1c0d5354ef0fcccbe8'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

ok - seed: registry scope+projects, charter copied, clones+origins, no-mistakes init in subhome only
warning: secondmate design sync skipped before launch: primary default-branch commit cannot be resolved
ok - spawn: launches in the subhome with persistent charter, records routing meta
ok - send: a bare fm-<id> secondmate enqueues a marked request and rings the meta window
ok - handoff: in-scope items move verbatim, out-of-scope stays, idempotent
ok - recovery: respawns from the durable registry and persistent home
ok - teardown: removes the home, then clears meta and the registry route
Evidence: fm-remote-backlog-handoff with decoy (pass)

Source: fm-remote-backlog-handoff with decoy (pass)

ok - confined put rejects incomplete and superseded payload generations
ok - confined put rejects directory replacement without external writes
ok - ambiguous receipt leaves one durable outbox and no duplicate dispatchable source
ok - re-delivery after unknown completion converges without duplication
ok - dropped transfer recovery overwrites scratch and delivers exactly once
ok - concurrent handoffs serialize staging through confirmed cleanup
ok - receiver removes one proven dead stale lock and retries once
ok - bootstrap detects pending outbox handoffs without a journal
ok - remote handoff releases durable work and separately retries its receiver wake
ok - an escalated undelivered receiver wake stays retryable instead of jamming the outbox
ok - a permanently unconfirmable wake never jams later durable handoffs
ok - fresh remote work gets a new wake after confirmed cleanup recovery
ok - confirmed marker cleanup cannot fail a completed remote handoff
ok - delivered pending state cannot suppress a new handoff wake
ok - unrecordable best-effort wake state drops without blocking later handoffs
ok - undeletable invalid wake state cannot block remote handoffs
ok - route classification serializes with retirement before staging
ok - unconfigured bootstrap has no remote handoff behavior
ok - a cwd .tasks.toml for another backend does not redirect a markdown receipt
ALL TESTS PASSED
Evidence: fm-backlog-atomicity with decoy (pass)

Source: fm-backlog-atomicity with decoy (pass)

ok - backend resolution preserves unreadable configuration errors for every caller
ok - backend resolution preserves environment, project, user, and default precedence
ok - backlog readers and mutations refuse unresolved backends without touching either backlog
ok - captain hold refuses backend errors and preserves relocated addressing for valid configuration
ok - dispatch publishes the record and moves the backlog item In flight in one run
ok - dispatch omits the markdown file when probing a Beads backlog
ok - a leftover markdown symlink does not brick a Beads home's dispatch
ok - completion applies and closes a Beads backlog without any markdown file
ok - dispatch refuses to supersede a pending authoritative close
ok - dispatch refuses held rows before creating resources
ok - dispatch refuses dependency-blocked rows before creating resources
ok - dispatch refuses held In-flight rows before relaunch
ok - dispatch reads backlog rows from the backlog addressing root
ok - recovery uses the parent of a trailing-slash data record
ok - completion targets nested relative data from the caller directory
ok - an immediate-child absolute data path keeps one paired backlog
ok - bare relative data addresses one backlog through dispatch and completion
ok - dispatch refuses symlinked backlogs without crossing homes
ok - automatic homes refuse lifecycle mutation without compatible tasks-axi
ok - dispatch refuses an unresolvable backlog data directory
ok - completion refuses before mutation when backlog data is unresolvable
ok - dispatch refuses, before creating anything, when the home has no item for the id
ok - dispatch distinguishes backlog read failures from missing items
ok - dispatch refuses a closed item instead of silently reopening it
ok - dispatch cannot commit without a verified task-record publication
ok - a failed backlog transition fails the dispatch loudly and leaves no record
ok - dispatch reports when failed-transition rollback cannot remove its record
ok - dispatch verifies both task and busy records during rollback
ok - dispatch commits neither record nor backlog state before launch delivery succeeds
ok - dispatch retries interrupted transitions before honoring termination
ok - a signal-deferred spawn verifies its preserved state and repairs a row that did not move
ok - a signal-deferred spawn reports attempted preservation as attempted when it cannot be verified
ok - a signal-deferred spawn bounds its verification so an unresponsive tasks-axi cannot hold the meta lock forever
ok - fm_tasks_axi bounds the call through its perl watchdog when no timeout binary exists
ok - fm_tasks_axi's perl watchdog passes the child status and output through unchanged
ok - fm_tasks_axi fails closed rather than running unbounded when no bounding mechanism exists
ok - fm_tasks_axi's GNU timeout kills a child that ignores SIGTERM after one further bound
ok - Kimi readiness interruptions fail before backlog commit
ok - dispatch does not resurrect a row closed after preflight
ok - dispatch fails when its backlog row vanishes after preflight
ok - completion closes a local-only ship, with its landing note, before reporting success
ok - completion closes a scout item against its report
ok - completion leaves legacy records open when no incarnation can be recorded
ok - completion refuses ambiguous task incarnations
ok - completion records relocated scout reports relative to the backlog root
ok - space-containing scout report paths round-trip through recovery
ok - control-byte data paths fail closed before paired transitions
ok - unreplayable data paths are refused before destructive cleanup
ok - completion preserves recovery state when task-record removal fails
ok - completion refuses to report success while its item is still open, and records what it owes
ok - restart recovers closes recorded before destructive cleanup
ok - completion refuses directory-symlink close targets
ok - completion reports failure until its durable close marker is removed
ok - session start retries a close whose marker could not be removed
ok - session start reports owned backlog rows it cannot read
ok - Orca cleanup recovery is excluded from backlog lifecycle transitions
ok - session start marks an item In flight when this home already owns a worker for it
ok - session start rejects internal worker-record symlinks
ok - session start rejects symlinked worker records
ok - session start finishes a close an interrupted cleanup recorded but never landed
ok - session start replays a recorded Gerrit close with its change URL as a note
ok - recovery backfills recorded links onto already Done items
ok - session start preserves a pending close across a transient backlog read failure
ok - recovery preserves incomplete-cleanup evidence across a failed replay
ok - session start finishes a close for the matching meta incarnation
ok - recovery preserves closes for ambiguous task incarnations
ok - recovery preserves both records when meta removal fails
ok - recovery preserves closes beside symlinked metadata
ok - recovery binds close-marker identity to its locked filename
ok - recovery rejects close markers targeting another home's data
ok - recovery validates an unterminated final marker field
ok - recovery rejects lexical traversal before resolving marker data
ok - recovery rejects marker control bytes before parsing
ok - recovery rejects malformed PR URL values
ok - a retained pending close is never started by reconciliation
ok - recovery rejects close-marker arguments outside its protocol
ok - recovery reports and rejects dangling symlink close markers
ok - session start drops a close recorded for an older meta incarnation
ok - session start rejects an unversioned close marker
ok - bootstrap rechecks worker-record containment after locking
ok - lifecycle files reject ancestor symlinks outside home roots
ok - same-home state overrides remain supported
ok - bootstrap refuses symlinked state before reconciliation
ok - bootstrap stops when backlog data disappears before reconciliation
ok - bootstrap preserves secondmate, manual, and absent-backlog exemptions
ok - session start leaves a captain-held item where the captain put it
ok - no-backlog teardown refuses symlinked records before resource actions
ok - teardown rechecks record parents after locking
ok - teardown refuses symlinked state before resource actions
ok - a home with no backlog remains exempt from lifecycle transitions
ok - spawn refuses a special-file tasks-axi config instead of blocking on it
ok - spawn refuses an unsafe tasks-axi config before any exemption is derived from it
ok - spawn refuses a data directory symlinked outside the home
ok - a configured adapter refuses a data directory outside the home
ok - dispatch and completion transition structurally with evidence
ok - refused teardown leaves the backlog item in flight
ok - environment-selected adapters bypass the legacy markdown file override
ok - a manual-backlog home dispatches and completes without a hard failure
ok - a secondmate home keeps its own books paired through dispatch and completion
ok - dispatching a persistent secondmate needs no backlog item
Evidence: Base commit fm-backlog-handoff with decoy (RED reproduction)

Source: Base commit fm-backlog-handoff with decoy (RED reproduction)

not ok - handoff to a live receiver failed: error: "beads show failed: exit 1: Error: no beads database found\nHint: run 'bd where' to inspect the resolved workspace, or 'bd init' to create a new database\n      or set BEADS_DIR to point to your .beads directory"
code: UNKNOWN
help[1]: "Check the beads CLI and workspace, then retry"
error: tasks-axi mv failed; nothing was moved.
Evidence: Base commit fm-secondmate-lifecycle-e2e with decoy (RED reproduction)

Source: Base commit fm-secondmate-lifecycle-e2e with decoy (RED reproduction)

Note: switching to '072a7da3eee2632fc02a6e0674d7601ca25a30ba'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

ok - seed: registry scope+projects, charter copied, clones+origins, no-mistakes init in subhome only
warning: secondmate design sync skipped before launch: primary default-branch commit cannot be resolved
ok - spawn: launches in the subhome with persistent charter, records routing meta
ok - send: a bare fm-<id> secondmate enqueues a marked request and rings the meta window
error: "beads show failed: exit 1: Error: no beads database found\nHint: run 'bd where' to inspect the resolved workspace, or 'bd init' to create a new database\n      or set BEADS_DIR to point to your .beads directory"
code: UNKNOWN
help[1]: "Check the beads CLI and workspace, then retry"
error: tasks-axi mv failed; nothing was moved.
not ok - handoff failed for in-scope items
Evidence: Handoff suite with --backend markdown removed (fails)

Source: Handoff suite with --backend markdown removed (fails)

not ok - handoff to a live receiver failed: error: "beads show failed: exit 1: warning: no beads configuration found in ~/.beads; using default database name \"beads\"\nError: failed to open database: embeddeddolt: init schema: embeddeddolt: open db: failed to load database \"beads\": openat LOCK: path escapes from parent"
code: UNKNOWN
help[1]: "Check the beads CLI and workspace, then retry"
error: tasks-axi mv failed; nothing was moved.
Evidence: Remote suite with receive flag removed (fails)

Source: Remote suite with receive flag removed (fails)

ok - confined put rejects incomplete and superseded payload generations
ok - confined put rejects directory replacement without external writes
handoff output:
stored: state/handoff/ios.outbox.md bytes=149
error: "beads show failed: exit 1: warning: no beads configuration found in ~/.beads; using default database name \"beads\"\nError: failed to open database: embeddeddolt: init schema: embeddeddolt: open db: failed to load database \"beads\": openat LOCK: path escapes from parent"
code: UNKNOWN
help[1]: "Check the beads CLI and workspace, then retry"
error: atomic backlog receipt failed; delivered outbox is preserved for retry
error: handoff receipt by ios was unavailable or completion is unknown; outbox preserved at /tmp/fm-remote-handoff.8DzV22/parent/data/handoff/ios.outbox.md
not ok - remote atomic receipt created no destination backlog before the dropped acknowledgement

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 markdown on the explicit-file tasks-axi mv/rm calls in fm-backlog-handoff.sh:746, :830, :1075 and fm-backlog-receive.sh:83) has no regression test. Nothing puts a decoy beads .tasks.toml in 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.toml and 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 containing backend = &#34;beads&#34; 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.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
With a decoy beads .tasks.toml in the checkout, fm-backlog-handoff and fm-secondmate-lifecycle-e2e pass ✅ pass live fm-backlog-handoff.decoy.log and fm-secondmate-lifecycle-e2e.decoy.log (both exit 0)
Remote handoff/receive and atomicity suites stay green under the same decoy checkout config ✅ pass live fm-remote-backlog-handoff.decoy.log, fm-backlog-atomicity.decoy.log (exit 0)
Base commit reproduces the reported RED under the decoy ('no beads database found') ✅ pass live fm-backlog-handoff.base-decoy.log, fm-secondmate-lifecycle-e2e.base-decoy.log (exit 1, beads error)
Adversarial: handoff from a cwd with a beads .tasks.toml fails once --backend markdown is removed from fm-backlog-handoff.sh ✅ pass live fm-backlog-handoff.flag-dropped.log (exit 1, 'tasks-axi mv failed; nothing was moved')
Adversarial: receipt fails once --backend markdown is removed from fm-backlog-receive.sh ✅ pass live fm-remote-backlog-handoff.receive-flag-dropped.log (exit 1)
  • Wrote a decoy beads .tasks.toml (db=/nonexistent/fm-decoy/beads.db) into the worktree checkout
  • bash tests/fm-backlog-handoff.test.sh with decoy -> exit 0
  • bash tests/fm-secondmate-lifecycle-e2e.test.sh with decoy -> exit 0
  • bash tests/fm-remote-backlog-handoff.test.sh with decoy -> exit 0
  • bash tests/fm-backlog-atomicity.test.sh with decoy -> exit 0
  • Base 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 markdown from bin/fm-backlog-handoff.sh + bin/fm-backlog-receive.sh -> fm-backlog-handoff exit 1, fm-remote-backlog-handoff exit 1
  • Mutation: 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 ✅
  • ⚠️ linter found issues (exit code 1)

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

✅ **Push** - passed

✅ No issues found.

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.
@kunchenguid
kunchenguid merged commit 4cca7d0 into kunchenguid:main Oct 11, 2026
19 checks passed
@kunchenguid

Copy link
Copy Markdown
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants