Merge upstream round 4 through kunchenguid/firstmate@0866a770 - #267
Merged
Merged
Conversation
…3247) * feat(extensions): bind trusted external process-event adapters * no-mistakes(review): Enforce owner and remote-home conformance * no-mistakes(review): Enforce serialized remote extension package lifecycle * no-mistakes(review): Enforce identity-conditional extension retirement * no-mistakes(review): Serialize extension retirement and recover crash cuts * no-mistakes(review): Unify retirement worker and lifecycle lock ownership * no-mistakes(review): Harden extension lifecycle retirement serialization * no-mistakes(review): Unify extension registration and overridden-state lifecycle boundaries * no-mistakes(document): Clarify built-in-only captain answer routing * no-mistakes(lint): Captain: fix extension binding ShellCheck findings * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes(review): Use isolated UID mapping for owner conformance * no-mistakes(review): Captain: remove forbidden CI ownership wrapper * no-mistakes(review): Serialize extension binding publication * no-mistakes(review): Document ordinary CI owner-fixture exclusion * no-mistakes(review): Quarantine orphaned handshake descendants * no-mistakes(test): Fix orphan attribution * no-mistakes(test): Harden process tracker baseline * no-mistakes(test): Harden detached descendant attribution * no-mistakes(test): Use exact invocation-group cleanup * no-mistakes(test): Bound remote conformance transport crossings * no-mistakes(test): Parallelize isolated extension conformance tests * no-mistakes(test): Lifecycle suite still exceeds deadline * feat(extensions): bind trusted external process-event adapters * no-mistakes(review): Enforce owner and remote-home conformance * no-mistakes(review): Enforce serialized remote extension package lifecycle * no-mistakes(review): Enforce identity-conditional extension retirement * no-mistakes(review): Serialize extension retirement and recover crash cuts * no-mistakes(review): Unify retirement worker and lifecycle lock ownership * no-mistakes(review): Harden extension lifecycle retirement serialization * no-mistakes(review): Unify extension registration and overridden-state lifecycle boundaries * no-mistakes(document): Clarify built-in-only captain answer routing * no-mistakes(lint): Captain: fix extension binding ShellCheck findings * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes(review): Use isolated UID mapping for owner conformance * no-mistakes(review): Captain: remove forbidden CI ownership wrapper * no-mistakes(review): Serialize extension binding publication * no-mistakes(review): Document ordinary CI owner-fixture exclusion * no-mistakes(review): Quarantine orphaned handshake descendants * no-mistakes(test): Fix orphan attribution * no-mistakes(test): Harden process tracker baseline * no-mistakes(test): Harden detached descendant attribution * no-mistakes(test): Use exact invocation-group cleanup * no-mistakes(test): Bound remote conformance transport crossings * no-mistakes(test): Parallelize isolated extension conformance tests * no-mistakes(test): Lifecycle suite still exceeds deadline * no-mistakes(review): Split extension conformance and forward remote transfer input * no-mistakes(review): Forward malformed remote payloads through fm-on * no-mistakes(review): Bound extension coordinator failure cleanup * no-mistakes(test): Skip repeated orphan sweep in coordinator children * no-mistakes(test): Queue isolated extension sections through bounded workers * no-mistakes(test): Bound extension coordinator lane cleanup * no-mistakes(test): Split remote lifecycle coordinator sections * no-mistakes(test): Coordinator probes pass; aggregate deadline remains * no-mistakes(test): Launch extension sections concurrently * no-mistakes(test): Fix coordinator marker publication * no-mistakes(test): Stabilize extension binding coordinator timing * no-mistakes(lint): Fix extension binding ShellCheck warnings * fix(extensions): prove invocation cleanup before retirement * no-mistakes(review): Harden process-event inbox confinement * no-mistakes(review): Preserve legacy capture parity * no-mistakes(review): Protect external registry staging * no-mistakes(test): Stabilize bounded extension conformance aggregate * no-mistakes(document): Document external evidence confinement * no-mistakes(ci): CI phase fixed. The failure was a flaky fixture in `tests/fm-remote-transport-lanes.test.sh`: its “fresh/in-use” staging directory had no live owner identity, so the real worker correctly reaped it once the 1-second age boundary elapsed on slower CI. The fixture now records the active test shell’s exact PID/start identity and cleans those records before removal. Verified: `bash tests/fm-remote-transport-lanes.test.sh` exits 0 with all checks passing; `git diff --check` passes. Provider check retrieval was also retried successfully, resolving the selected manual CI finding. Changed file: `tests/fm-remote-transport-lanes.test.sh` * no-mistakes(review): Harden extension staging and lifecycle reservation * no-mistakes(review): Harden external staging and lifecycle reservations * no-mistakes(review): Wire capture helper into remote conformance * no-mistakes(review): Pin external capture handoff and signal failures * no-mistakes(review): Bind pinned capture authority to inherited descriptor * no-mistakes(review): Harden descriptor-bound capture authority * no-mistakes(review): Harden core capture reservation authority * no-mistakes(review): Harden capture reservation boundaries * no-mistakes(review): Harden capture reservations and cleanup * no-mistakes(review): Harden capture handoff and reservation cleanup * no-mistakes(review): Bind capture handoff to claim descriptors * no-mistakes(review): Release lifecycle locks after host crashes * no-mistakes(review): Pin reservation recovery to recorded state roots * no-mistakes(review): Reject control bytes in claim state roots * no-mistakes(test): Stabilize extension capture descriptor handoff * no-mistakes(document): Document extension capture authority boundary * no-mistakes(lint): Fix ShellCheck extension binding warnings * no-mistakes(ci): CI phase result: fixed `bin/fm-procevent.sh` by initializing the shared `capture_state` sentinel for built-in adapters under `set -u`. This prevents normal built-in captures from aborting before publication. Verified: `bash -n bin/fm-procevent.sh` and `git diff --check` pass. The focused process-event suite was run locally but stopped earlier at a local detached-runner claim failure (`reconcile never claimed the registered source`), before the CI-reported post-capture path; CI evidence confirms the fixed unset-variable failure affected the failing remote, board, watcher, and process-event checks * no-mistakes(document): Correct extension namespace creation timing * no-mistakes(lint): Initialize capture locals for ShellCheck
* fix(bin): deliver the real definition of done to a promoted scout, and ban --yes A promoted scout used to receive a free-form placeholder instead of the mode-specific Definition of done a briefed ship worker gets, so it never saw the ask-user escalation rule or the --yes prohibition. That gap is the concrete reason one incident's worker drove validation with --yes and answered its own ask-user findings. - Add bin/fm-dod-lib.sh as the single owner of a ship task's mode-specific Definition of done, rendered by both bin/fm-brief.sh and bin/fm-promote.sh so the two contracts cannot drift. - bin/fm-promote.sh now writes data/<id>/ship-instructions.md carrying the scratch inventory, clean base, ship branch, and that Definition of done, and prints the fm-send.sh command that delivers it. - State the --yes ban as a prohibition rather than a preference, without claiming an enforcement the tool does not provide. - Cover both through the real promotion and brief paths in tests/fm-task-delivery.test.sh and tests/fm-brief.test.sh. * no-mistakes(review): Publish promotion instructions before committing task state * no-mistakes(review): Supersede conflicting scout delivery rules after promotion * no-mistakes(review): Reject invalid promotion instruction destinations * no-mistakes(document): Align documentation with promotion delivery contracts * no-mistakes(ci): Fixed both CI findings. Promoted workers now receive an explicit worktree-isolation check before branch creation, with instructions to stop and escalate if they are in the primary checkout. Updated behavioral coverage to verify the delivered promotion payload, and aligned the ask-user authority test with the new fleet-wide --yes prohibition. Verified with bin/fm-lint.sh, tests/fm-brief.test.sh, tests/fm-ask-user-authority.test.sh, tests/fm-task-delivery.test.sh, and git diff --check * no-mistakes(ci): Made tests/fm-ask-user-authority.test.sh executable so the modified colocated behavioral test runs directly like the surrounding test suite. Verified bin/fm-lint.sh, fm-brief, ask-user-authority, and task-delivery tests; all pass. git diff --check is clean * no-mistakes(ci): Strengthened tests/fm-task-delivery.test.sh to behaviorally verify that real promotion and brief generation deliver byte-identical Definition-of-done blocks for all three modes. Verified tests/fm-task-delivery.test.sh, tests/fm-brief.test.sh, bin/fm-lint.sh, and git diff --check. The outer pipeline can now commit and attest the updated head * no-mistakes(ci): Fixed promotion isolation instructions so any checkout other than the launched disposable worktree requires escalation, including another non-primary worktree. Updated behavioral coverage against the delivered promotion payload. Verified fm-task-delivery, fm-brief, fm-ask-user-authority, full fm-lint/ShellCheck, workflow lint, and git diff checks
) * fix(bin): present complete Lavish board feedback as structured output Give the Lavish adapter a read-only presentation so a handler sees every annotation and the session-ending tag=message as its own field, instead of grepping a truncated raw capture. * no-mistakes(review): Preserve unquoted messages and prioritize captain prose * no-mistakes(document): Document structured Lavish result reads * no-mistakes(ci): Fixed Lavish `read` completeness: rows missing declared fields are excluded from presented items, counted as malformed, and force `complete: no`. Added behavioral regression coverage through the adapter interface. `bin/fm-lint.sh`, syntax checks, and focused valid/malformed read checks passed. The portable-serial failure was an unrelated secondmate cooldown timing flake
* fix(records): pair backlog transitions with the record that moves Dispatch and completion each moved a task's physical record and its backlog row as two independently timed steps, so a crash or a forgotten follow-up could leave the two disagreeing: a record with no in-flight row, an in-flight row with no owner, or a finished task still shown in flight. Fold each backlog transition into the script that performs the physical change, under the per-task lock it already holds and before it reports success. Dispatch moves the item to In flight after publishing the task record and fails loudly, removing its provisional record, when that transition cannot land. Completion records an authoritative close and performs it before removing the record, so an interrupted cleanup can be finished later, and its closing message now confirms what already happened rather than instructing a future step. Add a same-home reconciliation sweep to session start so a home that was interrupted mid-transition settles its own books on restart, replaying a recorded close and restoring an in-flight row it already owns a worker for. It never reads or writes another home; the fleet snapshot and the cross-home nudge stay as backstops. Close records are validated before they are trusted: the file is read as raw bytes and rejected outright when it carries a NUL or other control byte, every field must be well formed and non-duplicated, the id must match the record it was found under, the data location must resolve inside this home, and each close argument must carry a permitted, well-formed value. Writer and reader share one validator so a record this home publishes always remains replayable, independent of locale. Homes configured for a manual backlog, and homes with no backlog at all, stay exempt and are unaffected. * no-mistakes(review): Remove stale bootstrap migration helper invocation * no-mistakes(review): Preserve pending closes and narrow signal deferral * no-mistakes(review): Record close before destructive teardown * no-mistakes(review): Refuse pending closes before creating resources * no-mistakes(review): Guard relaunches and preserve cleanup warnings * no-mistakes(review): Reject symlinked records and clarify cleanup guidance * no-mistakes(review): Align dispatch eligibility and protect close replay * no-mistakes(review): Unify exact task incarnation parsing * no-mistakes(review): Render resolved configured backlog path * no-mistakes(review): Harden transition path boundaries against symlinks * no-mistakes(review): Validate lifecycle state before resource actions * no-mistakes(review): Enforce transition tooling and continuous state locks * no-mistakes(review): Consolidate same-home lifecycle file boundaries * no-mistakes(review): Enforce canonical lifecycle containment and tooling contracts * no-mistakes(review): Reject final-component lifecycle record symlinks * no-mistakes(document): Document lifecycle record path boundaries * no-mistakes(lint): Quote literal done tokens in atomicity tests * no-mistakes(ci): Fixed all PR-caused CI failures: bootstrap now treats an absent state directory as an empty fresh home while retaining unsafe-state checks; nested remote secondmate retirement accepts records already removed with the retired home; teardown fixtures now provide valid data/manual-backend configuration; and the manual reminder assertion checks the configured absolute backlog path. Verified the reported tests, remote lifecycle E2E, backlog atomicity suite, Bash syntax, diff checks, and ShellCheck. The documented pre-existing captain-hold failure was intentionally untouched * no-mistakes(ci): Fixed Behavior portable serial 3 by adding `od` to the teardown test’s lsof-free PATH fixture. The new close-record validator legitimately requires `od`; its omission caused teardown to fail before process-group cleanup and stall the shard. Verified the full `tests/fm-teardown.test.sh` suite passes, plus Bash syntax, ShellCheck, and `git diff --check` * no-mistakes(ci): Fixed close replay to durably retain incomplete-cleanup evidence before removing task metadata. Subsequent retries now emit the reconciliation warning even after a backlog probe or close failure. Updated the behavioral regression and verified the full atomicity suite under stock macOS Bash 3.2, plus shellcheck and diff checks * fix(records): validate record bytes without an uncurated tool The byte validation added for close records and directory paths shelled out to od. The spawn and teardown lifecycle runs under a curated command set that deliberately excludes it, so on any restricted PATH the check could not run, the data directory read as unresolvable, and dispatch and cleanup refused - wedging the lifecycle rather than protecting it. An earlier attempt made the failing test pass by adding od to that curated set. That fixed the test to agree with the defect and quietly widened the contract the fixture exists to pin, so it is reverted here. Inspect the bytes with perl instead, which is already in the curated set and already used in this repo for the same portability reason. The emitted values are identical to od's, so the rejection semantics are unchanged: NUL and other control bytes are still refused, legitimate paths containing spaces or non-ASCII characters still round-trip, and the check stays independent of the process locale. The restricted-PATH teardown case now passes because the validator no longer needs od, not because the fixture was loosened. * no-mistakes(review): Enforce dispatch eligibility and atomic remote record publication * no-mistakes(document): Document dispatch eligibility and cleanup alerts
…3342) * fix: publish promote and Relay meta rewrites through contained replace Bare mv still rewrote live task records in place, so a symlink meta could be followed to a target outside state/. Route those field rewrites through the shared publisher and drop the unused library aliases. Co-authored-by: Cursor <cursoragent@cursor.com> * no-mistakes(review): Refuse dangling symlinks during X metadata clear * no-mistakes(review): Refuse unsafe metadata before follow-up and promotion side effects * no-mistakes(review): Exercise dangling symlink refusal through clear helper --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…d#2877) * fix(watch): absorb a turn-end whose pane churned since the previous poll The watcher's "absorb a benign turn-end when the crew is provably working" triage was structurally unreachable for any harness whose semantic busy state has no verified source. crew_absorb_class only reports working for an actively running no-mistakes step or an exact busy verdict, and bin/fm-crew-state.sh can only answer unknown for such an adapter, so codex crewmates surfaced a signal wake at every turn boundary with nothing to act on - a full supervisor drain, inspect and acknowledge turn per worker turn, scaling with the number of workers in flight and drowning the wakes that matter in identical noise. Widen the proof rather than bound the wake rate. A wake carrying only bare turn-ended markers is now also benign when the task's pane content changed since the previous poll, compared against the same state/.hash-* marker the staleness backbone already records and already trusts as liveness. That evidence claims no harness semantics, so it fabricates no busy verdict an adapter has not earned, and it needs no adapter cooperation. Absorb stays evidence-driven in both directions. A wake naming any status file keeps the strict proof, every captain-relevant verb still surfaces immediately, and an unresolvable task, a missing prior hash, a failed or empty capture, or an unchanged pane all surface exactly as before. The absorb defers rather than swallows: a crew that has stopped renders nothing further, so its now-static pane surfaces through the staleness backbone within a poll or two. Bounding the surfacing rate instead would have suppressed genuinely stopped workers. The derivation lives with the .hash-* marker format in bin/fm-watch.sh, which owns it, and costs one bounded capture reached only for a no-verb turn-end whose crew is not already provably working. * no-mistakes(review): Captain, guard pane-churn absorption from collisions and secondmates * no-mistakes(review): Captain, make watcher marker identities injective * no-mistakes(review): Captain, isolate ambiguous legacy markers and restore Herdr sourcing * no-mistakes(review): Captain, localize pane-churn collision guard * no-mistakes(review): Captain, reject malformed pane-churn hashes * no-mistakes(document): Document pane-churn turn-end evidence * no-mistakes: apply CI fixes * fix(watch): gate and bound the pane-churn turn-end absorb Make the pane-churn form of positive work evidence opt-in per home and bound how long it may defer one endpoint's bare turn-ends. Absorbing a bare turn-end on pane churn is now reached only when the home creates config/turnend-churn-absorb. The other two proofs read a verdict the harness itself vouches for, while this one infers execution from rendered bytes, so widening the absorb is a home's choice rather than a default every fleet inherits. With the flag absent the predicate returns on its first line and triage is unchanged. Churn and pane staleness read the same pane, so neither can be the other's only backstop. A pane that renders continuously never presents the two consecutive identical hashes the staleness backbone needs, so an unbounded churn absorb left a worker that had genuinely stopped behind such a renderer with no path to surface at all. One endpoint's turn-ends may now ride churn evidence for at most FM_TURNEND_CHURN_ABSORB_SECS, tracked in state/.churn-since-*, after which the wake surfaces and the window restarts. The bound is evaluated before any .stale- state is touched, so a wake that surfaces there leaves the staleness backbone's own classification alone. Covers both with behavioral tests: the same churning fixture that absorbs with the flag surfaces and queues without it, and a spent deferral window surfaces and restarts. The four existing safety guards now run with the flag enabled so they keep proving their specific guard. * no-mistakes(review): Fail closed on invalid churn deferral state * no-mistakes(review): Validate persisted churn deadlines before arithmetic * no-mistakes(review): Make churn deadlines transactional and bounds safe * no-mistakes(review): Compose turn-end evidence per task from one snapshot * no-mistakes(review): Restore strict turn-end fallback guards * no-mistakes(document): Clarify pane-churn supervision documentation * no-mistakes(lint): Fix watcher arithmetic lint issues * no-mistakes: apply CI fixes * no-mistakes(document): Clarify pane-churn fail-closed documentation * fix(bin): prioritize active pipeline-owned crew runs (kunchenguid#3194) * fix(bin): bind the live pipeline-owned run instead of a superseded failed row fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead of the LIVE replacement run: the live run's pipeline-owned lane head is not a git object in the task worktree, so head-equality attribution rejected it and the coarse runs-list fallback silently continued past the RUNNING row onto an older failed row whose head equalled the stale worktree HEAD. The home summary then flipped invalid and Bearings hid the home's live work (F10). Attribution precedence now follows the daemon's own identity: - An ACTIVE run for the task's branch binds without head equality while branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active); the pipeline owning the branch is itself the attribution. - A genuinely failed run with no later run on the branch still reports failed through the unchanged head-equality path - real failures are not hidden. - In the coarse runs scan, an unresolvable head is unknown attribution and stops the scan (fm_nm_head_resolvable) instead of falling through to an older row; a resolvable-but-mismatched head keeps the historical reused-branch skip. The exemption never applies to a terminal run and requires pipeline_owned specifically, both pinned by negative-control tests. Fixture shape verified against the live incident run's real axi status output. * no-mistakes(document): Updated run-attribution documentation ownership * no-mistakes(review): Captain, make watcher marker identities injective * no-mistakes(review): Captain, localize pane-churn collision guard * no-mistakes(review): Compose turn-end evidence per task from one snapshot * no-mistakes(review): Restore strict turn-end fallback guards * no-mistakes(document): Align pane-churn watcher documentation * no-mistakes(ci): Captain, fixed the flaky cooldown boundary test by freezing its executable clock. The failure reproduced before the fix and passed five consecutive full-suite runs afterward. Extended ShellCheck passed; full lint stopped because actionlint 1.7.12 is not installed --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
* fix(bin): add a safe owner for custom-check retirement Agents were improvising rm of check files with unset STATE/ID, which wedges headless panes. Unregister validates the id and state directory first. Co-authored-by: Cursor <cursoragent@cursor.com> * no-mistakes(review): Refuse explicitly empty custom-check state overrides * no-mistakes(document): Document custom-check retirement safety contract --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Round 4 of the TRACK sync plan: the contiguous upstream range from merge base 9e3df47 to 0866a77, seven first-parent changes, taken as one merge with no cherry-picking, rebasing, or squashing. Three collisions were resolved on meaning rather than by side: - The definition of done. Upstream's new bin/fm-dod-lib.sh is adopted as its one owner and extended to carry this fork's fragments, so a promoted scout and a briefed worker still receive the same contract and the fork's canonical ready-to-validate handoff survives. Upstream's stronger --yes ban is taken. A new regression drives the trigger with the handoff line taken out of a real generated brief, so the two files cannot drift apart silently again. - The atomic backlog transitions. Cleanup still publishes its completion manifest, captures task knowledge, and appends its model watermark before the close that removes the record, and a refusal that deliberately preserves a task now withdraws the pending close it recorded on the way in. - Dispatch rollback. A claude watermark-capture failure keeps its published record rather than stranding a live endpoint and local copy with nothing naming them; every other post-publication failure keeps upstream's rollback. The Lavish adapter, the process-event skill, and the bootstrap and watcher prose are unions of both intents. docs/fork-divergence.md records the three divergences this round creates.
…ixture its bound owner The three divergences the merge creates - the extended definition-of-done owner, the pending-close withdrawal on a preserving refusal, and the retained record on a watermark-capture failure - are recorded in the fork divergence ledger. Upstream's remote extension fixture mirrors a fixed list of bin/ files onto its fake remote root. This fork's bin/fm-remote-job-lib.sh sources bin/fm-timeout-lib.sh, which that list does not carry, so the remote entrypoint refused before it could publish the binding. Adding the owner to the list makes the remote transport case exercise what it was written to exercise.
Upstream's atomic transitions already carry a regression for the dispatch half of the secondmate exemption: a mate spawns with no backlog row and must succeed. Retirement is the other half, and it is the half this round moved, because cleanup now routes the record removal through the validated transition. Removing the kind exemption from fm_backlog_transition_applies makes a real secondmate retirement exit non-zero with "its backlog item could not be closed atomically", after the record is already gone, and leaves a pending close a session start would keep retrying for an agent that was never a work item. This case asserts the successful retirement, the absent pending-close record, and the absent backlog row, so that path cannot regress silently.
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.
Round 4 of the TRACK sync plan. One upstream merge of the contiguous first-parent range from measured merge base
9e3df47bto endpointkunchenguid/firstmate@0866a770234502364c268a768cb7c66cc321c629, seven changes, on fork base0f364f17dada691c2bde5d2a7e244e21fbba64e0.No cherry-pick, no rebase, no squash, no force. The merge commit's parents are exactly
0f364f17and0866a770. Upstream had advanced past the plan's ceiling at intake and that did not widen this round; re-planning the tail is a separate decision.Applicability of the seven changes
kunchenguid/firstmate#3247feat(bin): add trusted process-event extension bindings (1fbc7bb1).agents/skills/process-event-sources/SKILL.mdanddocs/captain-hold-lifecycle.mdmerged as unions with this fork's Lavishartifact-missingverdict kept beside upstream's new genericclassifyrouting andreadcommand. Upstream's remote extension fixture mirrors a fixed list ofbin/files onto a fake remote root; this fork'sbin/fm-remote-job-lib.shsourcesbin/fm-timeout-lib.sh, which that list did not carry, so the remote entrypoint refused before it could publish a binding. The owner was added to the list. This change also carries an inherited red described under "Attributed failures" below.kunchenguid/firstmate#3269fix(bin): deliver safety rules to promoted workers (c7fdef92)kunchenguid/firstmate#3321fix(bin): present Lavish feedback as structured output (debe4bfa)bin/fm-procevent-lavish.shgains upstream'sreadcommand beside this fork'scanonical_artifact_pathand gone-path retire. Both sides edited the header andusage(); the header is the union and the fork's growth-proofusage()(which prints every leading comment line) is kept over upstream's hardcodedsed -n '2,107p'line range, because upstream's range no longer covers the merged header.kunchenguid/firstmate#3322fix: keep task records and backlog transitions atomic (1260adce)kunchenguid/firstmate#3342fix(bin): contain promote and Relay metadata publishing (d71f4b9c)kunchenguid/firstmate#2877fix(bin): absorb turn-end wakes during bounded pane churn (a56a78ac)bin/fm-watch.shhad no code conflict. The two conflicts were both header prose, resolved as unions soconfig/turnend-churn-absorbis described beside this fork's first-sighting, keyed-open-decision, and declared-wait absorb rules.docs/architecture.md,docs/configuration.md, andAGENTS.mdcarry the same union.kunchenguid/firstmate#3369fix(bin): safely unregister custom checks (0866a770)bin/fm-check-unregister.shis new;AGENTS.mdsection 7 anddocs/scripts.mdgained its contract row beside this fork's existingfm-check-register.shsentence.C2 - who owns the definition of done (change 22)
Upstream moved the definition of done into a new
bin/fm-dod-lib.shrendered by bothbin/fm-brief.shandbin/fm-promote.sh, so a promoted scout receives the same delivery contract a briefed worker does. That library hardcodesfm/<task-id>, knows no design kind, and hands a no-mistakes worker off withdone: {summary}.This fork renders four things it cannot: the
--continue-branchtask branch (HelloWorldSungin/firstmate#149), the ADR design kind (HelloWorldSungin/firstmate#126), the configured declared-wait verb, and the canonicalblocked: implemented and committed, ready to validatehandoff thatbin/fm-trigger-validation.sh'scanonical_handoffresolves. That string exists nowhere upstream.Resolution: upstream's library is adopted as the owner and extended, rather than the fork keeping an inline copy. It now exposes
fm_dod_fragments,fm_dod_fragments_continue_branch,fm_dod_render, and upstream'sfm_dod_block.bin/fm-brief.shsets the fragments its task shape needs and renders;bin/fm-promote.shstill callsfm_dod_block, which is that same rendering for a default ship task. The declared-wait verb is resolved frombin/fm-classify-lib.sh's own constant rather than restated. Upstream's stronger--yes/-yprohibition is taken in full, in both the ship and ADR renderings and inCONTRIBUTING.md.Extending the library rather than keeping the fork's inline rendering was chosen because it is the only option that gives a promoted scout the fork's handoff too. Under the alternative, a promoted no-mistakes worker would still have reported
done:and never reached this fork's validation trigger.Behavioural proof, not file presence. A new regression,
test_generated_brief_handoff_is_the_line_the_trigger_closesintests/fm-trigger-validation.test.sh, generates a real ship brief, takes the handoff status line out of it with no restatement, appends that line, runsbin/fm-trigger-validation.sh, and asserts the open-decision fold is empty afterwards. It was proved to fail on drift: rewording the handoff inbin/fm-dod-lib.shto "ready for validation" turns it red withthe trigger did not close the handoff the generated brief tells a worker to append: blocked: implemented and committed, ready for validation. That is the exact silent break this collision risked, and nothing else in the suite caught it. Upstream's own byte-identity check between the promote and brief paths stays green, so the two renderings still cannot drift.The generated brief's only other change against fork
mainis upstream's--yeswording; the golden fixture and the exact design-DOD expectation were refreshed to match, and every other byte of all six brief variants is unchanged.Change 24 - atomic backlog transitions
Upstream makes
bin/fm-spawn.shandbin/fm-teardown.shrun thetasks-axi start/donetransitions themselves throughbin/fm-backlog-transition-lib.sh, records a pending close instate/<id>.backlog-closebefore destructive cleanup, and replaces teardown'srm -fof the task record with aclose.Taken, with the ordering this fork requires:
publish_outcome_manifest, the GBrain capture it wraps, and the model-watermark append all still run before the close that removes the record, and$STATE/$ID.metawas removed from the later receiptrm -flists on both the main and remote-secondmate paths so the validated transition owns it.test_interrupted_destructive_cleanup_leaves_a_recoverable_closepins by SIGTERM-ing teardown during the worktree return. This fork's cleanup also carries refusals upstream has no equivalent of that deliberately keep a task: an unpublishable completion manifest, a model-routing verdict that gained a turn, and a Herdr close that could not be confirmed gone. Left as-is, the next session start would replay the pending close and remove the very record those refusals exist to preserve.withdraw_pending_backlog_closeis called at exactly those refusal sites, and deliberately not from the exit trap, which a signal would also reach. A new colocated regression,test_a_preserving_refusal_withdraws_the_close_it_recordedintests/fm-backlog-atomicity.test.sh, drives a real teardown whose manifest cannot be published and then runs a real bootstrap, asserting the item stays In flight and the record survives the restart. Both halves now pin each other.bin/fm-spawn.shclears its rollback intent on exactly those two refusals. The backlog item stays Queued, which upstream's own worker-record reconciliation converges to In flight at the next locked session start, so the state this leaves is one upstream already knows how to correct. Every other post-publication failure keeps upstream's rollback unchanged, andtest_claude_watermark_failure_preserves_recoverable_metadatapins the retained record.Two other adaptations this change forced:
tests/fm-teardown.test.sh's twotasks-axi doneprompt cases are superseded by upstream's two backlog-close cases and were dropped with the functions upstream deleted; upstream'sseed_backlog_in_flightandbacklog_row_statehelpers are carried, and the fixture record now writesspawn_gen=beside this fork'sbranch=.tests/fm-endpoint-binding-migrate.test.sh's fixture home gained adata/directory, because cleanup now resolves the home's backlog directory before it can reach the binding refusal that test is about.Where the trees disagreed with the round plan
The plan (
data/fm-upstream-issue-reconcile/report.md, dated 2026-09-02) lists 40 paths changed on both sides for this round. The measured set is 41:tests/fm-secondmate-reconcile.test.shis also in it, and it conflicted. Both sides had written the same four-hour cooldown boundary assertion in different shapes; upstream's, which pins the reconcile's clock through a fakedate, replaced this fork's minute-wide bracket, because pinning the clock is the stronger form of the same assertion rather than a workaround for host load.Everything else in the plan's sections 3.3, 3.4, and 4 for this round matched what the trees actually contained, including its prediction that change 26 would not overlap the fork's watcher changes.
Test and lint evidence
All three runs used the same command shape on this host: every portable CI lane (
portable-parallel-1,portable-parallel-2, and everyportable-serial-<k>of<n>shard) run concurrently, plusbin/fm-lint.sh. Parent baselines were measured in isolated temporary clones so this branch and its merge structure were never touched.0f364f170866a770bin/fm-doc-audience-check.sh(126 surfaces, 717 local links) andbin/fm-pointer-check.sh(32 checked, 0 broken) both pass on the merge result.Attributed failures
Every one of the merge result's eight failures reproduces on the upstream parent with the identical assertion text, and none of them fails on the fork parent:
0f364f170866a770fm-procevent.test.shreconcile never claimed the registered sourcefm-procevent-when.test.shthe winning concurrent arm did not produce an outcomefm-remote-reply.test.shprocess-event runner never claimed the remote reply sourcefm-watch-triage.test.shthe fixture captured no process-event resultfm-remote-secondmate-lifecycle-e2e.test.shremote reply source did not capture the correlated answerfm-captain-hold-lifecycle.test.shbin/fm-procevent.sh start fixture-srcfm-bearings-board.test.shthe order-proof board build failedfm-extension-binding.test.shlocal bind returned no binding retirement identity,an isolated extension conformance section failedThe first six share one root cause and it was bisected: running
tests/fm-procevent.test.shin an isolated clone of the upstream line passes at9e3df47b(this round's merge base) and fails at1fbc7bb1(change 21). The minimal reproduction is two commands in a scratch home:The claim runs inside the re-executed
_startchild, so the failure is inside upstream's new claim path rather than anything this fork touched, and it does not depend onFM_PROCEVENT_CLAIM_ROOT(an isolated claim root fails the same way).fm-extension-binding's two remaining reds areerror[mode-unsafe]: ... mode is unsafe: 775, which is this host'sumask 002rather than a code fault.Settled by this PR's own CI: all eight were host-specific. Every behaviour lane is green on head
ece09b03d220fd0e00eed9c025bf82c34e48d79b- both portable parallel shards, all eight portable serial shards, the required real-Herdr lane, and the stock macOS Bash compatibility lane - so none of these eight failures reproduce in a clean environment. They are this development host'sumask 002, its live process-event claim root, and its installed herdr, not defects this round carries. The bisection to change 21 still stands as a local-environment sensitivity worth knowing about; it is not a reason to hold this round.Nothing was widened, skipped, or quieted to make a red pass. The merge result has fewer failures than the upstream parent because six of upstream's fourteen (
fm-bootstrap,fm-watcher-lock,fm-on,fm-remote-doctor,fm-session-start,fm-test-run) are shapes this fork already diverges on and they pass here.The fork parent's single failure,
fm-backend-herdr.test.sh(container_ensure should echo ..., got empty), passes on the merge result. It is a flake in that suite against a real herdr on this host, not something this round repaired.Divergence survival
Every active entry in
docs/fork-divergence.mdnames the test that pins it, and all of those pinning tests are inside the portable lanes this sweep ran. Their verdicts on the merge result:fm-agy-adapter.test.shfm-lint.test.shfm-quota-sidecar.test.shfm-send-strict.test.shfm-backlog-handoff.test.shfm-watcher-lock.test.shfm-watch-triage.test.sh,fm-daemon.test.shfm-daemonpass;fm-watch-triagered only on the inherited process-event claim assertion above, with every wedge-hold, pause-streak, and live-declaration case passingfm-backend-herdr.test.shfm-remote-job-orphan-reap.test.sh,fm-remote-job.test.shfm-test-run.test.sh,fm-on.test.shfm-crew-state.test.shfm-home-summary-refresh.test.shfm-pi-watch-extension.test.shfm-no-mistakes-required-gate.test.shfm-pr-merge.test.shfm-agents-hard-rules.test.shfm-bootstrap.test.sh,fm-brief.test.sh,fm-gbrain-capture.test.sh,fm-recall.test.shfm-dashboard-events.test.sh,fm-dashboard-gbrain.test.shBeyond those, the fork's ready-to-validate handoff is now proved end to end against the trigger by the new regression described under C2, rather than by observing that the string still exists in a file.
Ledger changes
docs/fork-divergence.mdgains three entries, all created by this round:No entry was retired and no entry's intent changed.
Parked branches
docs/fork-divergence.mdlists five deliberately unlanded branches:fm/fm-afk-injection-wedge,fm/fm-crew-state-blind-during-fix-round,fm/fm-parked-decision-stale-noise,fm/fm-subagent-model-routing-guard, andfm/fm-vault-drift-check. None entered this round. This branch descends only from0f364f17and0866a770, which the merge commit's two parents show.Contract decisions (firstmate, not the captain)
Both
AGENTS.mdcontract questions raised by changes 22 and 24 were put to firstmate and decided by firstmate. The captain was not consulted on either.1. Who moves the backlog item - adopt upstream. Sections 7 and 10 agree, with "ship, design, or scout" restored where upstream says "ship or scout", because upstream has no design kind while this fork's gate does cover it. Firstmate's reasoning: change 24 was taken as code, so the spawn genuinely does move the item now, and keeping the old section 7 sentence would leave the always-loaded contract describing behaviour the code no longer has - a false statement there is worse than either option. Beyond that, "update the backlog on every dispatch" was a documented promise rather than a mechanism, and upstream converts it into something that refuses; an enforced mechanism is not reverted back to an instruction someone has to remember.
Secondmate exemption, verified rather than assumed. A persistent secondmate is deliberately never a backlog work item in this fork, so firstmate required proof that upstream's refuse-without-an-item path does not catch one before this lands.
fm_backlog_transition_appliesreturns a skip onkind = secondmateas its first check, before any data resolution. Inbin/fm-spawn.shthe entire refuse-without-a-row block sits inside that gate'sthenbranch, and theelsebranch refuses only on the unresolvable-data status a secondmate can never reach.bin/fm-teardown.shresolves the same gate from the record's own kind.test_a_persistent_secondmate_is_never_a_backlog_itemspawns a secondmate into a home with no backlog row for it and asserts the spawn succeeds and publishes its record. It passes on the merge result.test_retiring_a_persistent_secondmate_needs_no_backlog_item, retires a realkind=secondmaterecord in a home that has a real backlog with no row for it, and asserts the retirement succeeds, records no pending close, and adds no backlog row.fm_backlog_transition_appliesmakes the same retirement exit non-zero withits backlog item could not be closed atomically (Task "..." not found in this backlog), after the record is already gone, leaving a pending close a session start would keep retrying for an agent that was never a work item.No fork adaptation to the exemption itself was needed; it was already correct on both paths, and only the retirement half lacked coverage.
2. Restating the definition of done - no change. Section 7 is left exactly as it was. The sentence about the canonical ready-to-validate
blocked:entry is still true of the merged code, and firstmate's reasoning is that section 7 owns the contract,bin/fm-dod-lib.shnow owns the rendering, and the new regression owns the enforcement: three owners, one fact. Naming the library in an always-loaded file at its size ceiling would create a second owner for a mechanism detail and buy nothing executable, which is the documented-promise defect wearing the other face.Expected red check
This round ships direct-PR, deliberately. The no-mistakes pipeline rebases onto
origin/main, which would replay and linearize a merge-only branch and destroy exactly the structure this PR exists to create. The fork's compliance workflow still requires a no-mistakes signature, soPR must be raised via no-mistakesis red by design. Every other required check must pass.Head
Pushed head:
ece09b03d220fd0e00eed9c025bf82c34e48d79bMerge commit:
601db8e7817d43c624c881e01897096b0a66feac, parents0f364f17dada691c2bde5d2a7e244e21fbba64e0and0866a770234502364c268a768cb7c66cc321c629Mergeability checked against
mainat that head:mergeable: true.mergeable_stateisunstable, which is the by-design red compliance check below rather than a conflict.CI verdict on that head: 16 of 17 checks pass, and the single failure is
PR must be raised via no-mistakes, which this direct-PR round carries deliberately for the reason stated below.