merge: upstream sync round 1 of 7 through kunchenguid/firstmate@4f89f5b5 - #248
Merged
Merged
Conversation
…#3140) * fix(bin): stop dropping reconcile nudges for markerless remote secondmates A persistent remote secondmate's parent-side state/<id>.meta never carries spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and never writes one, because that incarnation identity does not apply to a remote route. fm-secondmate-reconcile.sh's row filter required a non-empty spawn_gen matching an identifier regex, so every such row was silently dropped before the per-row loop ever saw it: no sent/stale/failed line, no cooldown record, nothing sent, and no trace of why. Give a legitimately markerless persistent remote secondmate a safe substitute identity - its recorded remote_host - instead of weakening the spawn_gen check for rows that do have a generation: - bin/fm-secondmate-reconcile.sh: carry host through the row projection for both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty spawn_gen instead of filtering the row out. A new revalidate_identity() compares the sampled spawn_gen against current metadata when one was sampled (unchanged), or the sampled host against the metadata's remote_host when none was sampled and the metadata still carries no spawn_gen of its own. A row with neither a spawn_gen nor a host has no safe identity at all and fails loudly instead of vanishing, exactly the visibility the original bug lacked. - Rows now join on the ASCII unit separator rather than @TSV: bash's IFS-whitespace read collapses consecutive tabs, which would have silently dropped a legitimately empty field again. - bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile projection so the fm-bearings.v1 path (the one bearings itself feeds to the reconcile hook) carries the same substitute identity. - tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a genuinely seeded remote home) for a markerless mate nudged once per cooldown window, a stale/replaced remote route refused exactly like the existing local spawn_gen case, and a row with no identity at all failing loudly rather than being swallowed. * no-mistakes(review): Enforce markerless remote host identity during final delivery * no-mistakes(document): Document markerless remote reconciliation safety
…decorated (kunchenguid#3147) * fix(watch): hand a busy declared pause to the away-mode daemon undecorated While away mode is active the daemon owns triage and the watcher reverts to one-shot, handing over plain wake identities the daemon classifies itself. The busy-turn bound was the one stale path that did not: with afk active it ran the wedge timer, so the daemon received a wake already decorated as a possible wedge. That decoration outranks the daemon's own verdict. handle_wake escalates an enriched wedge reason before its pause classification can apply, so a crew that declared the wait itself - a `paused:` external wait or a verified captain-held transfer holding a live foreground call - was wedge-escalated once per FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count climbing into demand-deep-inspection on a pane nobody needed to inspect. Measured on the pre-fix tree, five consecutive re-arms produced five escalations. busy_turn_bound_check now reads the declaration before the afk branch: away mode hands off the plain window identity, one-shot per distinct stale hash, leaving normal-mode pause bookkeeping unwritten because the daemon owns it there. The daemon then classifies the wait itself and self-handles it on the long cadence. Normal-mode behavior is unchanged, and lifting the declaration still restores the busy-pane wedge escalation on the same pane. The regression covers all three: the undecorated handoff with no wedge timer or escalation counter, the one-shot on re-arm that the escalation ladder used to climb, and the restored wedge escalation once the declaration is lifted. * no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state * no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits * no-mistakes(document): docs: note afk busy-bound handoff in watcher header --------- Co-authored-by: Talon Stark <talonstark@gmail.com>
…unchenguid#3121) * fix(bin): explain a pooled slot's stale submodule refusal A pool slot whose submodule pin moved is refused with "is not clean; refusing to discard uncommitted work", while the operator's own `git status` in that slot reads clean. The message names no submodule, no pin, and no remedy, so the refusal is unreadable and the slot looks wedged for no reason. That is the failure that jammed three slots in a row when a submodule pin moved. The refusal itself was never the bug and is unchanged: the gate still refuses, and still touches nothing. It now distinguishes the one case it can prove and says what it found - the submodule, the pin the slot has, the pin the base records, and the command that clears it. The diagnosis is deliberately conservative, because ` M <path>` alone cannot tell a stale pin from real work. An entry is reported as stale only when every reported entry is a gitlink whose submodule is internally clean and whose recorded pin actually differs. A submodule holding uncommitted work, untracked files, or an unpushed commit therefore keeps the original uncommitted-work refusal, even when its pin is also stale - the remedy command would be wrong there, and the conservative refusal is the safe answer. Nothing is converged, synced, initialized, or deleted. There is no new failure path: a slot that launched before still launches, a slot that refused before still refuses, and projects that configure a submodule `ignore` are read exactly as before. Paths are read with core.quotePath=false so a non-ASCII submodule is named rather than falling back to the unreadable message. Tests keep the reproductions that prove the message is accurate: the stale-pin diagnosis (which fails against the previous refusal), work inside a submodule still refused as uncommitted work, and a stale pin carrying real work refused conservatively rather than called stale. Each asserts the slot is left untouched. * no-mistakes(review): require remote containment before calling a submodule pin stale * fix(bin): stop printing a remedy the containment check cannot stand behind The stale-pin diagnosis printed `git submodule update --checkout` as the command that clears the slot. The containment check behind it reads local refs only and never fetches, because this gate has to stay usable offline. A remote-tracking ref that has gone stale - its upstream branch deleted or force-pushed, and never pruned - still reads as containment, so a commit that is really unpushed can look contained and that command would move the submodule off it. Naming the submodule and both pins is the whole point of the diagnosis: it turns "is not clean", on a slot whose own `git status` reads clean, into a statement of which submodule drifted and where it drifted from. The operator can choose the remedy from that, seeing the whole picture. Printing an instruction that rests on a judgement which can be fooled is worse than printing none, so it is dropped. The limitation is now stated where it applies, in the script header and beside the check itself, rather than left for a reader to discover. No fetch is added: the gate stays offline-safe by design. Nothing else changes - the same conditions are refused, the slot is still never touched, and a submodule carrying real work or an unpushed commit still keeps the conservative uncommitted-work refusal. * no-mistakes(review): bound submodule containment probe to first commit
* fix(pi): type captain supervision outcomes so main relays them A captain-relevant branch outcome reached main as a bare user message with no marker of origin or required action, written in main's own captain-facing voice, landing in a tail that often already held several such notes. Pi keeps only a custom message's content when it builds the provider request, so customType and display never reach the model and content was the only place that identity could live. Main could not tell an incoming outcome from its own earlier answer and sometimes re-emitted that answer instead of relaying the outcome, losing it. Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24 turns, rising to 3 in 6 once one stale answer was already in the tail, which is how one captain conversation saw six identical messages in a row. The same scenario with the outcome typed failed 0 times in 14 turns. Wrap only the captain-verdict note in the branch-outcome operational kind owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still display: false, still one triggerTurn follow-up, so the turn remains the single captain-visible outcome and no hidden note is ever shown twice. Routine notes stay plain because their renderer reads the glyph off the front of that same string. An outcome that cannot be encoded degrades to the same instruction as plain text rather than being lost, matching this file's stated failure direction. The existing assertions could not catch this: they pin the sendMessage options and never look at what main receives. Add a portable regression that classifies the delivered payload with the real protocol executable, and a live guard that runs the real Pi SDK's own convertToLlm to prove content is the entire model-visible payload. * no-mistakes(document): Document typed Pi captain outcomes
… or enriched wedge (kunchenguid#3155) * fix(bin): keep a busy pane from retiring a still-declared wait's window The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a busy pane as "the crew resumed" and dropped the declared-wait marker, without re-reading that the crew's own latest status line still declared the wait. That inference is not safe, because a declared wait can legitimately hold a pane busy: a worker sitting on a long foreground call keeps that call live for as long as the wait lasts. The marker is then cleared while the declaration still stands, and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very next tick, so the window restarts forever and the wait never matures into its one bounded recheck. Away mode makes that terminal. Since the watcher half landed, a busy pane under a declared wait is handed to the daemon exactly once per declaration and never woken again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so this recheck is the only thing left that can re-surface the pane at all. Measured end to end on a throwaway state root, away mode active, a pi pane busy past FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS windows: 0 captain-facing rechecks before this change, 6 after - one per window, with the marker reset each time. The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability check: exit code 2 still means the capture failed, so the endpoint is gone and the marker goes. The loop head above already drops the marker the moment the status line stops declaring the wait, so nothing else is needed to end the routing, and the reconcile path runs before the probe ever reads a pane. tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old inference on purpose - its fixture's status line still read `paused:` while the pane was busy, and its comment read "A pause whose pane became busy again (the crew resumed)". Its fixture now resumes the way a crew actually resumes, by appending a non-declaring status line, and it asserts its own busy verdict first so it cannot silently decay into the idle-pane case that test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the inverse guard: a busy pane must not GATE the clear either, so an over-correction that kept the marker alive whenever the pane is busy would fail it. test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over both declaration forms. It asserts the busy verdict, then that ticks inside the window neither escalate nor let the marker be recreated with a fresh timestamp, then exactly one recheck past the window named for the right human and never a wedge, then silence on the next tick inside the reset window. It fails on unmodified main with "produced 0 escalations past its window, expected exactly one". Refs kunchenguid#3149 * fix(bin): let a declared wait outrank an enriched wedge escalation handle_wake classifies a stale wake through classify_stale, which returns a `pause` verdict for a crew whose latest status line declares an external wait or a verified captain-held transfer. It then threw that verdict away whenever the wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's enriched wedge decoration outranked the crew's own declaration and a healthy declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the wait lasted. The enriched reason earns its precedence over the daemon's cheaper status-log absorption honestly - it carries the watcher's escalation count and its explicit "do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is not run-step or pane state. It is the crew's own declaration that this pane waits by design, which is precisely the question the wedge timer cannot answer for itself, so it is the one verdict that decoration must not override. The two classifications genuinely disagree in steady state rather than only in a race: a crew that declares `paused:` while its no-mistakes run is still attributed to its code reads `working` to the watcher's pause_state_class, so the watcher takes the wedge timer while the daemon's classify_stale reads the status log and correctly returns `pause`. The wait stays bounded, not silenced. Absorbing to the pause action records the declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS as an explicit "recheck whether the wait still holds", instead of once per FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root over five wedge cadences for one declared wait: 5 escalations climbing to demand-deep-inspection before this change, 0 after, with the one bounded recheck still delivered. tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's `paused` case pinned the old precedence on purpose, asserting exactly one escalation carrying the demand-deep-inspection payload. That case now asserts the pause cadence instead - no escalation inside the window, pause tracking recorded - while the `working` and `prior-terminal` cases keep asserting the enriched wedge verbatim, so the override itself is still pinned everywhere it is correct. test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression. It asserts the fixture's own classifier verdict is a pause first, so the case cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both the plain and demand-deep-inspection forms through the real handle_wake and housekeeping pair, then matures the window for exactly one awaiting-external recheck, then lifts the declaration and requires the same enriched wedge to escalate again unchanged. It fails on unmodified main at the first delivery. Refs kunchenguid#3149 * no-mistakes(review): align afk skill recheck wording with still-declared contract * no-mistakes(document): daemon doc comments: pause window ages on declaration --------- Co-authored-by: Talon Stark <talonstark@gmail.com>
* fix(bin): make Claude auto-arm continuity self-heal past a hung claim On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held the single-flight owner lock with its epoch ledger frozen at outcome=arming, and the abandonment proof read any live lock holder in arming as legitimately deciding forever. Every later Stop firing exited 0 at the lock, the turn-end guard kept deferring to the hung owner as recovery under way, and the watcher was never auto-re-armed again for the rest of the session - supervision survived only on manual arms and lapsed between them (the 2026-08-26 watcher flap). Corrections layered onto the lock-held-across-arm shape each reopened the same concurrency class one level down, so this replaces the claim machinery wholesale with a generation-based optimistic design: - The epoch ledger's monotonic sequence IS the claim generation; the two-line entry (classic epoch record plus the claimant's MANDATORY pid-identity) is the claim. Every firing defers to a live OPEN claim: outcome arming, owner alive, identity recomputes and matches, and not stuck (entry and watcher beacon both older than the guard grace). - A finished, dead, identity-mismatched, identityless, or stuck claim is superseded by simply taking the next generation - no signalling or revocation of a steady-state predecessor. - No mutex is held across arming or output; the owner lock survives only as a micro-mutex around individual ledger writes. A superseded owner goes completely silent: ownership is re-verified before every arm invocation, episode-state mutation, ledger write, and continuation. - The irrevocable commit point of a translation is the exit status (the harness delivers the collected stderr only on exit 2), so the owned terminal ledger write is the atomic commit: the winning generation exits 2 unconditionally after it, a refused one exits 0 silently even after printing, and the once-per-episode failure notice commits in the same owned critical section as the winning failed write. Two bounded residuals are documented accepted intent: an owner dying between its owned write and its own exit, and a hung old-build owner resuming during the one legacy upgrade window. - The pre-generation lock-holding claim shape keeps defer-or-reclaim behavior through a legacy shim: a live identity-verified stuck owner is retired via TERM (with a queued TERM sufficient when the owner is stopped) before its lock is removed, an unverified or identityless pid is never signalled but never blocks a proven-abandoned reclaim, and the lock's identity evidence is grafted into the ledger (mtime-preserving) so pid-reuse protection survives the lock. - The guard reads the same predicates for recovery ownership and its terminal fail-open (which re-checks for a live open claim under the held locks before committing the attended alarm), with ledger reads anchored to line 1 so the identity line can never confuse them. Behavioral regression coverage exercises all three edge classes through the real hook and guard - a live open claim defers with no lock held, a stuck claim is superseded and the home re-arms, and an end-to-end run with a genuinely hung owner shows a concurrent firing deferring promptly mid-arm, a later firing superseding the stuck owner, and the superseded owner exiting silently without a second translation - plus the identityless/reused-pid loopholes, the superseded-owner arm boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths. * no-mistakes(review): Refuse auto-arm commits when notice marker creation fails * no-mistakes(review): Make episode reset atomic with generation ownership * no-mistakes(document): Update auto-arm generation and commit documentation
…n unproved merge (kunchenguid#3064) * fix(pr): verify GitHub merge outcome * no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation * no-mistakes(document): Correct forge-specific merge documentation * no-mistakes(review): Captain: forge-only queue fix, focused tests pass * no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression * no-mistakes(review): Captain: remove history proof; retain executable regressions * no-mistakes(document): Clarify GitHub recording timing in architecture docs * no-mistakes(document): Clarify outcome-aware PR merge recording documentation * no-mistakes: apply CI fixes * Revert "no-mistakes: apply CI fixes" This reverts commit c326cfa. The automatic CI repair round removed the up-front `gh` prerequisite check while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls `gh api graphql` for the outcome read and `gh api` for the branch-rules read. That left the same hard requirement without the clear named error, and review immediately raised a new finding for exactly the failure the check prevents - `gh-axi pr merge` landing the merge while the follow-up read fails, so the PR metadata is never recorded. The check is also symmetric with the GitLab arm directly above it, which already refuses up front when `glab` or `jq` is missing, on the stated principle that a missing tool should be a named prerequisite rather than a merge that is armed and then refused for an unexplained reason. The workflows this round was chasing sit at `action_required` because this is a fork pull request; no code change can turn them green. * fix(pr): keep PR bookkeeping when a merge outcome read fails On the GitHub path a merge call that returned success was followed by `github_read_outcome || exit 1`, so a transient API failure, rate limit, or network blip during the read dropped out of the script before `record_pr_metadata` ever ran. The merge could have landed while `pr=` went unrecorded and the merge poll was never armed - bookkeeping lost on a real merge. The failure path just above already recorded metadata before exiting, so the error path was more careful than the success one. Record the PR before that refusal. Recording arms the later merge poll and is not a success claim, which is the same reasoning that keeps `record_pr_metadata` on the gh-axi failure path. The refusal itself is unchanged: exit stays non-zero and the message still names the concrete observed state. Metadata is withheld only when the read succeeds and proves the pull request neither merged nor queued. Pin it with a case that stubs `gh api graphql` into failure after a successful `gh-axi pr merge`, asserting both the non-zero exit and the recorded metadata. * no-mistakes(review): Aggregate queue rules and report conflicts explicitly * fix(pr): keep the merge abstraction reachable and its bookkeeping intact Two holes remained in the outcome-verified GitHub merge path, both on installations where gh-axi is present but gh is not. The verification preflight refused before bin/fm-pr-merge.sh ever reached the configured gh-axi merge abstraction, so an installation without gh could no longer merge at all. gh-axi now performs the merge unconditionally and the queue-aware gh read became an optional enrichment: with gh on PATH its GraphQL view still separates merged from queued, and without gh the gh-axi view still proves a landed merge while every outcome it cannot prove refuses. The PR metadata recording sat behind the outcome read, so a merge that landed before that read failed lost pr= and its merge poll. Recording now happens once, before either forge call, which arms the poll without claiming a landed outcome and leaves teardown a PR identity to verify against no matter how the read ends. Rebasing onto main also restored the durable merge-outcome reporting and the GitLab landed-state confirmation that the conflict resolution dropped. Tests pin each fix through the executable interface: the merge abstraction is reached and verified with gh absent, a failed fallback read keeps its bookkeeping, and a mock that snapshots the task meta during the forge call proves pr= is recorded before the merge can land. * no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts * no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal * no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it * no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe * no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge * no-mistakes(document): align merge docs with verified GitHub outcome contract
* fix(pi): stop reporting one merge to the captain twice The supervision branch's captain-outcome note told main, unconditionally, that the note "is not your own earlier output" and to relay it now. When main had already reported the same event, that assertion was false and the order turned the correct response - saying nothing new - into a mechanical re-report, so the captain saw one merge reported twice in 16 seconds. Two independent changes, both needed: - The relay instruction is now conditional. It still names itself as a supervision outcome so main cannot mistake it for its own earlier answer (the silent loss that instruction exists to prevent), and it now lets main stay quiet about an outcome it has already given the captain. - The merge case is closed at its source rather than left to that judgment. One merge reaches a home on two independent paths by design - main's own permanently main-owned merge poll, and the branch's task-local status wake - and main's captain-facing text only reaches the branch's mirror at main's turn end, so the branch can escalate before it could possibly see the captain was already told. bin/fm-pr-merge-notified.sh answers that question from bin/fm-pr-lib.sh's canonical merge-notification marker, so the answer holds regardless of mirror timing. A captain outcome naming an already-published merge is delivered as the ordinary rendered note instead of opening a follow-up turn: still appended, still visible, still recorded with the verdict the branch decided, minus the wasted turn. Any error, timeout, or unreadable state relays the outcome. A duplicate announces itself; a lost outcome does not. Regression coverage drives the real delivery path in both directions: a new outcome must still reach the captain in exactly one follow-up turn even beside an unrelated published merge, and an already-published merge must open no second turn while a different PR in the same task still does. The merge path's real producer and this new consumer are exercised end to end in tests/fm-pr-merge.test.sh. Pi-only by construction: the delivery path lives in .pi/extensions, so no other harness loads it, and the new script only reads existing markers. * no-mistakes(review): Document accepted latest-marker suppression residual * no-mistakes(review): Recheck ownership before merge outcome delivery * no-mistakes(document): Document merge-outcome suppression exception * refactor(pi): drop the source-level merge suppression, keep the envelope fix The captain reviewed this branch and judged the source-level duplicate suppression overly complicated for the problem it solved, and asked for the change to be reduced to the envelope wording alone. Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and every test and document that existed only for it. What remains is the conditional captain-outcome instruction: main is told to stay quiet about an outcome it has already reported and to relay anything else, which covers the duplicate without a second mechanism. The silent-loss protection is untouched - the note is still typed, self-describing, and delivered as one invisible follow-up turn - and the behavioral tests still assert that, now requiring both halves of the conditional instruction. * no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed * no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head
Take the contiguous upstream first-parent prefix 60bedde..4f89f5b as one merge, per the TRACK strategy in docs/fork-divergence.md. Eight upstream changes: markerless remote secondmate reconciliation (kunchenguid#3140), the busy declared pause handed to the away-mode daemon (kunchenguid#3147), the stale submodule pin named behind a pooled slot refusal (kunchenguid#3121), stale captain outcome re-emissions (kunchenguid#3154), a declared wait kept on the pause cadence (kunchenguid#3155), Claude auto-arm recovery from hung claims (kunchenguid#3156), the real GitHub merge outcome verified instead of reported unproved (kunchenguid#3064), and duplicate captain outcome reports (kunchenguid#3184). Eight paths conflicted and were resolved on meaning, keeping both intents wherever they were compatible: - AGENTS.md section 7: the guarded merge sentence now carries both the fork's work-item lifecycle clause and upstream's unproved-merge refusal. - .agents/skills/afk/SKILL.md, docs/architecture.md, docs/configuration.md: upstream's busy-pane declared-wait routing and status-line ageing beside the fork's self-widening recheck window and run-progress hold. - bin/fm-supervise-daemon.sh: upstream's contract that pane busy state does not end a declared wait, so housekeeping's `0)` busy arm is dropped and the fork's streak record moves to the surviving `2)` arm; the per-iteration pause_resurface_window replaces upstream's fixed read. - bin/fm-watch.sh: upstream's away-mode handoff branch adopted whole inside busy_turn_bound_check, keeping the fork's sixth hold-count parameter on the wedge_timer_check call below it. - docs/scripts.md: both clauses of the fm-pr-merge.sh row. - tests/fm-pr-merge.test.sh: every case from both sides, 79 assertions against 53 on the fork parent, 61 on the upstream parent and 35 at the merge base, so nothing was dropped or duplicated. tests/fm-dashboard-events.test.sh needed the same treatment without a textual conflict: kunchenguid#3156 adds the claim identity as a second line of state/.claude-autoarm-epoch, which two separate processes cannot share, so the emitter-equivalence comparison normalizes it beside the owner pid and stamp and asserts its presence separately rather than comparing vacuously. This merge does not resolve the merge-proof contract question that kunchenguid#3064 opens against the still-open #241; the merged behaviour is upstream's and the PR body records the contradiction.
Add the watcher's live declared-wait routing and self-widening recheck cadence (#144, #51), which bin/fm-watch.sh collided on this round while going unrecorded since its introduction. Record kunchenguid#3147's second collision with the run-progress wedge hold, and note under the dashboard entry that kunchenguid#3156 added a run-varying claim identity to the auto-arm epoch ledger, so a future round does not restore the un-normalized emitter-equivalence comparison.
… trap test_pi_process_exit_cleanup_stops_arm_child waited up to 20s for the cleanup log to show TERM had arrived, then asserted immediately that the child was gone. The child writes that log line from inside its own TERM trap, before its exit, so the line proves the signal arrived and not that the process is reaped. Between the two the trap body has to finish, bash has to tear down, and the kernel has to release the pid. Observed on this host during a full-suite sweep: "Pi arm child 537033 survived process-exit cleanup", while the same suite passes standalone. Give the death its own bounded wait on the same budget as the delivery wait above it. The assertion is unchanged in strength: with the fixture's trap edited to log TERM and deliberately not exit, the case still fails.
Fork main advanced from 0c51a08 to b9b86fe while this round ran, so the PR base moved and no longer merged cleanly. Bring the new base in as a merge rather than rebasing: the round's whole purpose is to advance the merge base against upstream, and a rebase would replay and linearize the upstream merge this branch exists to create. No upstream commit enters here. git log 0c51a08..origin/main is exactly one commit, fork PR #246, which carries no upstream content. One path conflicted, bin/fm-claude-stop-autoarm.sh, and only in its header comment. Both bullets are kept, each matched to what the merged code actually does: - Need: #246's expanded list (process-event sources and a pending wake queue), because fm_supervision_needed now reads FM_SUP_QUEUE_PENDING as a need input in bin/fm-supervision-lib.sh. - Single-flight: upstream kunchenguid#3156's generation-claim model, because fm_autoarm_claim_open and fm_autoarm_claim_next in bin/fm-wake-lib.sh are what the merged code calls; #246's lock-admits-one-owner wording describes a mechanism this merge already replaced. The suites both sides touch pass: fm-claude-stop-autoarm, fm-turnend-guard, fm-guard-stale-banner, fm-control, fm-backend-zellij.
…ence Firstmate decided on 2026-09-03, on this round's PR, that the fork keeps a merge proof a deferred execution does not satisfy, against upstream kunchenguid#3064 accepting a merge-queue entry as verified. The entry carries the reasoning rather than only the verdict, so the collision is not re-derived every round: this fleet has no merge queue for an enqueue to be deferred into, reporting an unproved merge as landed has actually happened here, and conforming to the more protective option needs nobody's permission while relaxing one does. It also records what the code does TODAY, which is upstream's permissive read, because round 1 merged kunchenguid#3064 faithfully. The stricter implementation is fork PR #241, held on a separate matter, and the reconciliation happens when that lands against this merged base. A future round must not read the merged code as evidence the fork chose upstream's contract. Retirement condition: if this fork ever adopts branch protection with a merge queue, re-examine the divergence rather than defend it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Round 1 of 7 of the upstream sync. This takes the contiguous upstream first-parent prefix
60bedde5..4f89f5b5into the fork as ONE merge commit, under the TRACK strategy indocs/fork-divergence.md. Fork base is0c51a080, measured merge base is60bedde5, endpoint is exactlykunchenguid/firstmate@4f89f5b5. No cherry-pick, no rebase, no squash, no force-push, no Sync fork button, and no upstream commit later than the endpoint.Land this with
bin/fm-pr-merge.sh <task-id> https://github.com/HelloWorldSungin/firstmate/pull/248 -- --merge. The script defaults to squash, and squashing here destroys the upstream parentage: the merge base would not advance and rounds 2 through 7 would re-present these same eight commits as new. The explicit--mergeis load-bearing.The PR base moved mid-round, and it was reconciled by merge, not rebase
Fork
mainadvanced from0c51a080tob9b86fe3(fork PR #246) while this round ran, and the branch stopped merging cleanly. The new base is brought in as a MERGE commit (1ff2d4b7), never a rebase: rebasing would replay and linearize the upstream merge this branch exists to create, which is the one thing the round must not do.No upstream commit enters through that reconciliation.
git log --oneline 0c51a080..origin/mainis exactly one commit, #246, which carries no upstream content. The round's upstream endpoint is still exactly4f89f5b5.One path conflicted,
bin/fm-claude-stop-autoarm.sh, and only in its header comment. Both bullets are kept, each matched to what the merged code actually does rather than to which side looked newer:fm_supervision_neededinbin/fm-supervision-lib.shnow readsFM_SUP_QUEUE_PENDINGas a need input.fm_autoarm_claim_openandfm_autoarm_claim_nextinbin/fm-wake-lib.share what the merged code calls. fix(bin): arm supervision for pending wake queues and bind zellij endpoints to the recorded tab #246's "lock admits exactly one owner" wording describes a mechanism this merge already replaced, so keeping it would have left the comment contradicting its own file.The suites both sides touch pass on the reconciled head:
fm-claude-stop-autoarm,fm-turnend-guard,fm-guard-stale-banner,fm-control,fm-backend-zellij.Applicability: every upstream first-parent change in this round
22fa6ed9kunchenguid#3140 markerless remote secondmate reconciliationbin/fm-secondmate-reconcile.sh,bin/fm-send.sh,bin/fm-bearings-snapshot.sh,docs/remote-secondmates.mdand two suites auto-merged with no fork hunk in range.99c1a0dckunchenguid#3147 busy declared pause handed to the away-mode daemonbin/fm-watch.shbelow.524994c8kunchenguid#3121 stale submodule pin named behind a pooled slot refusalbin/fm-spawn.shplus a new suite; the fork's spawn hunks (dashboard event emitters, agy gates) are elsewhere in the file.d63b0e2fkunchenguid#3154 stale captain outcome re-emissions.pi/extensions/fm-branch-supervision.ts,bin/fm-operational-input.shand their docs and suites auto-merged.5953e9b5kunchenguid#3155 declared wait kept on the pause cadence.agents/skills/afk/SKILL.md,bin/fm-supervise-daemon.sh,docs/architecture.mdanddocs/configuration.mdbelow.10b93b2ckunchenguid#3156 Claude auto-arm recovery from hung claimsbin/fm-claude-stop-autoarm.sh,bin/fm-turnend-guard.shandbin/fm-wake-lib.shauto-merged whole;tests/fm-dashboard-events.test.shneeded adapting, below.7ee0c192kunchenguid#3064 verify the real GitHub merge outcomeAGENTS.md,docs/scripts.mdandtests/fm-pr-merge.test.shconflicted and were resolved.4f89f5b5kunchenguid#3184 duplicate captain outcome reports (endpoint).pi/extensions/fm-branch-supervision.tsand its suite auto-merged.Nothing in this round was dropped, and nothing later than
4f89f5b5entered it:git log --first-parent 60bedde5..4f89f5b5is exactly these eight, and the merge commit's second parent is4f89f5b5e235469d32c037b7792d6dba5bdc272d.Mechanical proof that neither side lost content: for every file the merge touches, every line either side ADDED against the merge base is present in the merge result, except lines I deliberately rewrote to combine the two intents. Those exceptions are exhaustively:
bin/fm-pr-merge.sh: 0 absent from either side (206 fork-added lines and 375 upstream-added lines all present).bin/fm-supervise-daemon.sh,bin/fm-watch.sh,.agents/skills/afk/SKILL.md,AGENTS.md,docs/architecture.md,docs/configuration.md,docs/scripts.md: only the combined sentences, plus upstream'spause_secs=${FM_PAUSE_RESURFACE_SECS:-...}read that the fork's per-iterationpause_resurface_windowsupersedes, and the daemon's0)busy arm that fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge kunchenguid/firstmate#3155 exists to delete.tests/fm-pr-merge.test.sh: only the fork's matrix letters, which are re-lettered(ax)to(bf)after upstream's(aw); every case body from both sides is present.tests/fm-dashboard-events.test.sh: only the three lines of the normalization I adapted, described below.The merge-proof contract contradiction - DECIDED, and it must not be re-litigated
7ee0c192(kunchenguid#3064) and open fork PR #241 give two opposite answers to one question, and only one can survive. This collision was predicted at round-1 intake, surfaced from this PR, and decided by firstmate on 2026-09-03: the fork keeps the stricter contract.A future round must not silently resolve this by taking upstream's side. The merged code in this PR carries upstream's permissive read, because a faithful merge of this prefix is what round 1 is, and that merged code is NOT evidence that the fork chose upstream's contract. The decision, its three reasons, and its retirement condition are recorded in
docs/fork-divergence.mdunder "Stricter merge-proof contract". A collision re-litigated every round because nobody wrote the decision down is the exact failure mode that ledger exists to stop.Upstream (in this merge): a merge-queue entry IS a proved outcome.
bin/fm-pr-merge.shprintsverified: <url> is queued (...)and exits 0, and a queue-required refusal actively recommends--autoas the retry (retry with: ... -- --auto --<method>).Fork PR #241 (open,
fm/fm-stale-base-check-before-mergeatacc67921): "GitHub's live PR state must prove that neither a merge queue nor auto-merge can defer execution before the merge command runs", and "A queued or pending result is a refusal". It refuses--autooutright.The behaviour this merge actually has, proven by reading the merged function rather than by reasoning about intent -
bin/fm-pr-merge.sh, thegithub)arm:A merge-queue enqueue is accepted as
verified: queued, exit 0. Upstream's contract is what is merged here. The fork's work-item lifecycle block below that arm still runs only on a proved landing, because the queued path exits before reaching it.AGENTS.mdsection 7 states neither contract today, and this merge gives it upstream's: the sentence now reads "so merge metadata and any eligible explicitly recorded work-item lifecycle are handled and an unproved merge is refused instead of reported as landed" - the fork's clause and upstream's, both kept.The decision and why, recorded here and in the ledger:
maincarries no branch protection and no rulesets, verified against the API on 2026-08-14, so there is no queue for an enqueue to be deferred into. Upstream's design accommodates a workflow this fork does not have, so adopting it buys nothing while widening what counts as proof.Retirement condition: if this fork ever adopts branch protection with a merge queue, re-examine the divergence rather than defend it, because reason 1 stops holding the moment a real queue exists.
What happens next, and what does NOT happen in this PR: fork PR #241 carries the stricter implementation. It is open and held on a separate matter, and this sync round deliberately does not rebase or re-express it. The reconciliation happens when #241 lands against this merged base. Until then
bin/fm-pr-merge.shandAGENTS.mdsection 7 state upstream's contract, and the ledger entry says so explicitly rather than claiming a difference the code does not yet have.tests/fm-pr-merge.test.shconflicted for the same reason and is resolved to assert what the merged code actually does. No assertion was weakened or deleted - see the test evidence below.Per-file reasoning for every changed contract file
The eight paths that conflicted textually, plus the one that needed the same treatment without conflicting.
AGENTS.mdBoth sides edited the guarded-merge sentence in section 7. The fork's clause is the work-item lifecycle; upstream's is the unproved-merge refusal. They are compatible, so both are kept in one sentence. The hard-rule block did not conflict this round: hard rule 2 ("Never write to a third-party upstream repository") is still rule 2 and
tests/fm-agents-hard-rules.test.shpasses..agents/skills/afk/SKILL.mdSame bullet, edited additively on both sides. Upstream adds that a declared wait is tracked "whether its pane reads idle or busy", that this outranks an enriched possible-wedge reason, and that the window ages against the crew's own latest status line. The fork adds that the window is its own (self-widening) and that each unchanged recheck doubles it up to
FM_PAUSE_RESURFACE_MAX_STREAK. All five facts are kept; the only line dropped is the fork's earlier phrasing of the sentence upstream rewrote.bin/fm-supervise-daemon.shThe load-bearing resolution. kunchenguid#3155 exists to stop pane busy state ending a declared wait, so housekeeping's
0)arm (busy -> drop the marker) is deleted, exactly as upstream requires, and the fork's$streak_filecleanup moves onto the surviving2)(endpoint gone) arm - so the streak still shares the marker's lifetime and cannot be orphaned. Upstream's fixedpause_secs=${FM_PAUSE_RESURFACE_SECS:-...}read is dropped in favour of the fork's per-iterationpause_resurface_window "$(pause_streak_count ...)", which already sits inside the loop. The header, the env table, and the two block comments are combined so upstream's "the crew's own status line is the authority" and the fork's run-progress hold and self-widening cadence are both stated once.bin/fm-watch.shkunchenguid#3147 restructures
busy_turn_bound_checkso the declaration is read BEFORE the afk branch, and adds a declaration-keyed one-shot handoff to the away-mode daemon. That whole branch is adopted verbatim; the fork's sixth<hold-count-file>parameter (the run-progress wedge hold) is threaded back into the signature, thelocallist, and thewedge_timer_checkcall below the new branch. Every one of the eightwedge_timer_checkand twobusy_turn_bound_checkcall sites still passes six arguments. The header keeps the fork's "before any such wake arrives" wording, which the fork applies consistently at three places, with upstream's afk clause folded in.docs/architecture.mdTwo paragraphs, both additive on both sides. The fork's run-progress hold rationale and dead-agent short-circuit exclusion sit beside upstream's away-mode handoff paragraph; upstream's "declared-wait" vocabulary and its two new status-line-authority sentences are taken, with the fork's
crew_wedge_progressclause kept on the away-mode sentence.docs/configuration.mdUpstream changed only
FM_PAUSE_RESURFACE_SECS; the other three lines in the conflicted block are base text the fork had rewritten (FM_STALE_ESCALATE_SECS,FM_BUSY_TURN_MAX_SECS,FM_SECONDMATE_WAKE_STALL_SECS) plus two fork-only keys (FM_PAUSE_RESURFACE_MAX_STREAK,FM_SECONDMATE_WAKE_IDLE_STALL_SECS). The fork's block is kept whole and upstream's ageing clause is folded into the one line it actually changed.docs/scripts.mdOne table row, one clause from each side, both kept.
tests/fm-pr-merge.test.shReconstructed rather than hand-patched, because git interleaved two large additive rewrites. Upstream's file is the base, the fork's helper functions and
FM_CONFIG_OVERRIDEare re-inserted, and the fork's matrix entries are re-lettered(ax)-(bf)after upstream's(aw). The result is verified by test-function inventory and by assertion count, below.tests/fm-dashboard-events.test.sh- conflicting in substance, not in textGit merged this cleanly and the result was WRONG, which is why the round runs the full suite rather than trusting a clean merge. kunchenguid#3156 adds the claim identity as a second line of
state/.claude-autoarm-epoch, and that identity is a process start time plus a home-scoped command line - two separate processes cannot share it. The fork's emitter-equivalence case compares two homes' ledgers with onlyowner_pid=andupdated_at=normalized, so it started failing on a difference that is not a difference in what the auto-arm DECIDED:The identity line is now normalized too, and because normalizing it could hide a claim that recorded NO identity, its presence is asserted separately before the comparison runs. The
sedis anchored per line (1s/,2s/), so a differing outcome, epoch or marker on line 1 still fails; verified by driving the inputs apart by hand.One attributed test fix, and it is NOT part of the merge
Commit
1f1c69fe, a separate non-merge commit after the merge commit and the ledger commit. The merge commit itself carries only the merge and its conflict resolutions. This is called out with the same visibility as an upstream applicability row because a reviewer must not have to guess where it came from.Status: PRE-EXISTING, exposed by running the full suite under load. NOT introduced by this merge. Proven, not asserted:
tests/fm-pi-watch-extension.test.shis not in this merge's changed-file set (git diff --name-only 0c51a080 42765150does not list it).0c51a080:tests/fm-pi-watch-extension.test.shand42765150:tests/fm-pi-watch-extension.test.share both5ad729bb10af7089c6aa79d462288937f54a5028. Upstream's blob (c7b708d0) equals the merge base's, so upstream never touched the file either; the fork diverged it before60bedde5.0c51a080:2181-2185..pi/extensions/fm-primary-pi-watch.ts, the extension the suite drives, is untouched by this merge; the only.pi/files it changes arefm-branch-supervision.tsandlib/fm-operational-input.ts, which belong to a different suite.The defect.
test_pi_process_exit_cleanup_stops_arm_childwaits up to 20s for the cleanup log to record TERM, then asserts immediately that the child is gone. The child writes that log line from inside its own TERM trap, BEFORE itsexit 0:So the line proves the signal arrived, not that the process was reaped. Between the two the trap body must finish, bash must tear down, and the kernel must release the pid. The fix gives the death its own bounded wait, on the same 20s budget and the same
sleep 0.02shape as the delivery wait directly above it.Observed:
not ok - Pi arm child 537033 survived process-exit cleanup, during the full-suite sweep only. The same suite passed standalone before the sweep and eight consecutive times after the fix.Disconfirming check, so this is a fix and not a false pass. The fixture's trap was edited to log TERM and deliberately NOT exit, and the case still failed:
not ok - Pi arm child 1273671 survived process-exit cleanup. The assertion still catches a genuinely surviving child; only the race is gone.Family: this is a residual instance of #158, not of the watcher stop-path signal-delivery defect. #158 was remediated by fork PRs #166 ("make watcher arm assertions event-driven") and #167 ("add event-bounded path absence helper"), which swept wall-clock assertions into event-bounded ones - in this very file, among others. This case is the same defect class in the same file, missed by that sweep. It is not the same underlying thing as a bash signal-delivery defect in the watcher stop path: the delivery assertion PASSED here, so the trap parsed, fired, and wrote its line. Only reaping had not completed. These should not be fixed twice under two names, and they are not the same name.
Deliberately not done:
tests/lib.shhasfm_test_wait_absentfrom #167 for path absence, but no sibling for process death. Adding one is the natural follow-up if more process-death races surface. This round keeps the local loop that matches the sibling wait five lines above it rather than widening a shared test surface inside a merge PR.Divergence survival - behaviour, not file presence
Every active entry in
docs/fork-divergence.mdwas re-verified by running the suite that pins it against the merge result, not by checking that its files are still there.tests/fm-agy-adapter.test.shpasses.tests/fm-lint.test.shpasses.tests/fm-quota-sidecar.test.shpasses.tests/fm-send-strict.test.shpasses;bin/fm-send.shtook kunchenguid#3140's remote-delivery change with the reopen intact.tests/fm-backlog-handoff.test.shpasses.tests/fm-watcher-lock.test.shpasses.tests/fm-watch-triage.test.shandtests/fm-daemon.test.shpass;busy_turn_bound_checkstill carries the sixthholds_fileargument through towedge_timer_checkbeside kunchenguid#3147's new branch. Ledger updated with this second collision.tests/fm-backend-herdr.test.shpasses.tests/fm-remote-job-orphan-reap.test.shpasses.tests/fm-pi-watch-extension.test.shpasses.tests/fm-no-mistakes-required-gate.test.shpasses;.github/workflows/no-mistakes-required.ymlstill hastypes: [opened, synchronize, reopened]with noedited, on the per-PR concurrency group.docs/fork-divergence.mdand the sync tooling are untouched by upstream this round..no-mistakes.yamlstore_in_repore-read directly and stillfalse. This file does not conflict textually, so it is re-checked every round rather than trusted to a clean merge.tests/fm-agents-hard-rules.test.shpasses; hard rule 2 is still rule 2.CONTRIBUTING.mdhas no restored instruction to clone the parent or pointoriginatkunchenguid/firstmate.README.mdtakes only kunchenguid#3154's one-line Calm wording change; the fork's install-source divergence (#172) is untouched.tests/fm-gbrain-capture.test.shandtests/fm-recall.test.shpass.tests/fm-gbrain-readonly-e2e.test.shis its declared opt-in gate skip (FM_GBRAIN_LIVE_E2E=1), unchanged by this round.tests/fm-dashboard-events.test.shpasses after the ledger-normalization adaptation described above; that adaptation is now recorded in the ledger so a future round does not restore the un-normalized comparison.Ledger changes this round
Commit
c9b761e9:bin/fm-watch.shcollided on it here, and it was unrecorded from its introduction (fix(bin): honor declared pauses from live workers #144, fix(bin): stop claiming an unobserved forge merge and re-escalating parked waits #51) until now. The entry names the divergence, its field evidence, the upstream-owned functions it lives inside, and how this round's collision was resolved.Added "Stricter merge-proof contract" (commit
db69da0d), recording firstmate's 2026-09-03 decision on the kunchenguid#3064 collision, its three reasons, the gap between that decision and the code this round merges, and the condition that would retire it.No entry was retired. Every conflict was resolved by keeping both intents or by taking upstream's contract; the one deliberate divergence added is the decision above, which the code catches up to when #241 lands.
Parked branches
docs/fork-divergence.md"Parked branches outside the fork baseline" lists 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,fm/fm-vault-drift-check. None entered this round. The branch is exactly0c51a080plus one merge of4f89f5b5plus one ledger commit, so nothing else could have:git log --first-parent 0c51a080..HEADis two commits.Test and lint: baseline versus final
Baselines were established on BOTH merge parents in isolated temporary clones before the merge commit existed, so every failure in the final run is attributed to a parent or to this merge. Command in all three trees:
bin/fm-test-run.sh --all --exclude-family real-herdr-gated, andCI=true bin/fm-lint.shfor the full canonical lint set.Lint
0c51a0804f89f5b5Tests
0c51a0804f89f5b51ff2d4b7The full run is on
1ff2d4b7. The pushed head isdb69da0d, which adds exactly one commit on top: the merge-proof decision ledger entry, adocs/fork-divergence.md-only change with no code or test in it. CI on the pushed head is the check of record for that commit, and it is reported below.No regression. The merge result's failure set is exactly the fork parent's failure set. Nothing new is red, and nothing that was green went red.
The merge does not inherit the upstream parent's failures either. Five suites are red on
4f89f5b5and green on the fork parent -fm-bootstrap,fm-on,fm-public-followup,fm-remote-doctor,fm-session-start- and all five are green on the merge result, so the fork's own wiring survives rather than being displaced by upstream's tree.An earlier full run on
1f1c69fe(this head minus the base reconciliation) gave the same two failures, plus one suite,tests/fm-captain-hold-lifecycle.test.sh, that was interrupted by an external signal rather than failing: exit 130 (SIGINT) after 1316ms against a normal 32s, its last two cases reportedok, nonot okline, and no trailing completion marker - exactly the missing-final-line signal that convention exists to make visible. It was re-run three times green on that head, and it is green in the final run above.Attribution of every failure in the final run
Two lines are red in the final run. Neither is a regression introduced by this merge; both are red on the fork parent too.
tests/fm-pi-branch-extension.test.shtests/fm-test-run.test.shThe honest claim for this PR is "no regression against the fork parent", not "all green". A green-sounding claim that a spot check falsifies costs more than the accurate one, so the two known failures are named rather than papered over.
tests/fm-pi-branch-extension.test.sh- #221, fixed upstream, arriving in round 2Fails identically on the fork parent
0c51a080, the upstream parent4f89f5b5, and this merge, with the same assertion in all three:Calm-off ToolExecutionComponent rendering differs from Pi stock. It is a pi-version drift on this host, not a repo-red state - fork CI is green onmain, and the most recent CI run on the fork's ownfm/fm-stale-base-check-before-mergebranch passed.It is fixed upstream and arrives complete in sync round 2 as change
f66be0f8. The upstream reconciliation proved that on this host: atd22318eathe extension renders identically to stock Pi 0.84.2 for both a 2-line and a 12-line fixture, where our HEAD differs on both. The fix rides the whole pending.pi/extensionsrange and is not cherry-pickable, which is why it is round 2 work and deliberately not this round's.So this red line is a tracked item with a landing date, not an unexplained blemish, and nothing in round 1 should be held for it.
tests/fm-test-run.test.sh- #220, host environmentruby is required to parse .github/workflows/ci.yml as YAML. The suite hard-fails when ruby is absent instead of skipping, so it is red on any host without a system ruby. Pre-existing on the fork parent, untouched by this round, and green in CI where ruby is present.tests/fm-pr-merge.test.sh- proof the reconstruction lost nothingThe file was rebuilt from upstream's version plus the fork's additions, so it needs stronger evidence than "it passes". Assertion counts across the four trees:
ok -assertions60bedde50c51a0804f89f5b553 + 61 - 35 = 79 exactly. Every assertion unique to either side is present once, none is duplicated, and none is lost. The test-function inventory agrees: the only name absent relative to the union of both parents is
test_records_pr_and_head_before_merging, which upstream RENAMED totest_verified_merge_records_pr_and_headwith its four assertions byte-identical, and which upstream then supplemented withtest_metadata_recorded_before_merge_callprovingpr=is recorded before the forge call that can land the merge.The same inventory check was run on every other suite the merge touches. One other name is absent:
test_arming_claim_is_never_reclaimed, which kunchenguid#3156 deliberately removes because its premise ("an owner still arming is never reclaimed, however long the cycle runs") is exactly the hung-claim behaviour that change fixes. Upstream replaces it with eleven cases includingtest_arming_claim_with_fresh_beacon_is_never_reclaimed,test_fresh_arming_claim_with_stale_beacon_is_never_reclaimedandtest_stuck_generation_claim_is_superseded_and_rearms, so the surviving half of the old guarantee is pinned more precisely than before. No assertion was weakened or deleted by this merge.Pushed head and mergeability
db69da0d4368cb80d6ce15d1c8c87752f4828a274f89f5b5e235469d32c037b7792d6dba5bdc272d(kunchenguid/firstmate@4f89f5b5)0c51a080d8e7d0963465c9395eef7170704c1045(forkmainat dispatch)git log --first-parent 0c51a080..HEADis five commits: the upstream merge, the ledger update, the one attributed test fix, the base reconciliation merge, and the merge-proof decision ledger entry. No other commit can have entered.b9b86fe3(fork PR fix(bin): arm supervision for pending wake queues and bind zellij endpoints to the recorded tab #246)git merge-tree --write-tree origin/main HEADreports no conflicts againstorigin/mainatb9b86fe3, re-checked after the push.The one red check, by design
This round ships direct-PR, not through no-mistakes. The no-mistakes pipeline rebases onto
origin/main, which would replay and linearize a merge-only branch and destroy the upstream parentage this PR exists to create. The fork's compliance workflow still requires a no-mistakes signature, soPR must be raised via no-mistakesis expected to be RED on this PR and only that check. Every other required check must pass; if any other check is red, this PR is not ready.CI result on this PR: 16 passed, 1 failed, and the 1 is the expected one
gh-axi pr checks 248 --repo HelloWorldSungin/firstmate:PR must be raised via no-mistakesEvery other required check passes. This also empirically confirms the two local failures are host-local, not repo-red:
tests/fm-pi-branch-extension.test.shandtests/fm-test-run.test.shboth run in the portable serial lane, and all eight serial shards are green in CI.How to land it
Not squash. A squash would commit the merge-result tree as a single-parent commit, the merge base against upstream would stay at
60bedde5, and rounds 2 through 7 would each re-present these eight commits.🤖 Generated with Claude Code
https://claude.ai/code/session_01DH3SATKCsSPd7BDfj9DBJ2