Skip to content

sync: merge upstream kunchenguid/firstmate main (8 commits) - #36

Merged
knowttl merged 9 commits into
mainfrom
fm/fm-upstream-sync-16
Aug 27, 2026
Merged

sync: merge upstream kunchenguid/firstmate main (8 commits)#36
knowttl merged 9 commits into
mainfrom
fm/fm-upstream-sync-16

Conversation

@knowttl

@knowttl knowttl commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Sync the fork with its upstream parent kunchenguid/firstmate, absorbing the 8 commits the fork was behind while preserving the fork's own work (notably the atelier-axi rename of lavish-axi).

Absorbed commits

Conflict resolutions

Every conflict combined the fork's atelier-axi rename with the upstream change. No fork feature and no upstream feature was dropped.

  • .agents/skills/bearings/SKILL.md - keep the fork's /bearings atelier naming; fold in upstream's cooldown-limited reconcile instruction and its state/<id>.reconcile-nudged record.
  • .agents/skills/bootstrap-diagnostics/SKILL.md - take upstream's no-mistakes floor bump (1.31.2 → 1.46.0, structured pipeline attestation), keep the fork's atelier-axi in the axi-family list. Matches NO_MISTAKES_MIN=1.46.0 and COMMON_TOOLS in the cleanly merged bin/fm-bootstrap.sh.
  • bin/fm-watch.sh header and docs/architecture.md - both sides added an independent clause over the merge base. Keep the fork's wedge-timer working-deferral exemption (the nodefer path in busy_turn_bound_check) and add upstream's afk-daemon handoff clause; both hold in the merged code.
  • docs/configuration.md - upstream's no-mistakes floor plus the fork's atelier-axi rename, consistent with fm-bootstrap.sh.
  • docs/scripts.md - keep the fork's /bearings atelier board row, add upstream's new fm-secondmate-reconcile.sh row.

Leftover hunt

Upstream's new tests referenced the pre-rename lavish names, which would fail against the fork's atelier-axi code (.atelier/ path, atelier-axi binary). Translated to atelier in tests/fm-bearings-board-render.test.sh, tests/fm-bearings-board.test.sh, and tests/fm-watch-triage.test.sh. No lavish reference remains in the tree.

Verification

  • bin/fm-lint.sh clean.
  • Passing suites: fm-bearings-board, fm-bearings-board-render, fm-watch-triage (incl. the afk-handoff case), fm-secondmate-reconcile, fm-pr-merge, fm-send-remote-delivery, fm-inactive-reconcile, fm-task-inbox, fm-pi-branch-extension, fm-cursor-primary, fm-brief, fm-spawn-pool-base-freshen.
  • One fm-bootstrap failure (test_network_phase_partitions_the_run expecting MISSING: node) is environmental: this host has /usr/bin/node on the test base path, so removing the fake node does not hide it. The merge does not touch node detection and the case passes in CI.

thelad-dev and others added 9 commits August 26, 2026 11:40
…unchenguid#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…chenguid#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes
* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.
…chenguid#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes
…#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
Absorb the 8 upstream commits the fork was behind:

- d63b0e2 fix(pi): prevent stale captain outcome re-emissions (kunchenguid#3154)
- 524994c fix(bin): name the stale submodule pin behind a pooled slot refusal (kunchenguid#3121)
- 99c1a0d fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (kunchenguid#3147)
- 22fa6ed fix(bin): reconcile markerless remote secondmates safely (kunchenguid#3140)
- 60bedde fix(bearings): preserve projections through inventory mismatches (kunchenguid#3129)
- 662a8c7 fix(bin): durably report merged pull requests (kunchenguid#3104)
- 5aed873 fix(pi): make supervision model picker searchable and scrollable (kunchenguid#3099)
- b0ca8f5 fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identitaet stilllegen (kunchenguid#3115)

Conflict resolutions (all combine the fork's atelier-axi rename with the
upstream change; no fork feature dropped, no upstream feature dropped):

- .agents/skills/bearings/SKILL.md: keep the fork's /bearings atelier naming
  and fold in upstream's cooldown-limited reconcile instruction and its
  state/<id>.reconcile-nudged record.
- .agents/skills/bootstrap-diagnostics/SKILL.md: take upstream's no-mistakes
  floor bump (1.31.2 -> 1.46.0, structured pipeline attestation) and keep the
  fork's atelier-axi in the axi-family list. Matches NO_MISTAKES_MIN=1.46.0 and
  COMMON_TOOLS in the cleanly merged bin/fm-bootstrap.sh.
- bin/fm-watch.sh header, docs/architecture.md: both sides added an
  independent clause on top of the merge base. Keep the fork's wedge-timer
  working-deferral exemption (the nodefer path in busy_turn_bound_check) and
  add upstream's afk-daemon handoff clause; both hold in the merged code.
- docs/configuration.md: upstream's no-mistakes floor plus the fork's
  atelier-axi rename, consistent with fm-bootstrap.sh.
- docs/scripts.md: keep the fork's /bearings atelier board row and add
  upstream's new fm-secondmate-reconcile.sh row.

Leftover hunt: upstream's new tests referenced the pre-rename lavish names,
which fail against the fork's atelier-axi code (.atelier/ path, atelier-axi
binary). Translated to atelier in tests/fm-bearings-board-render.test.sh,
tests/fm-bearings-board.test.sh, and tests/fm-watch-triage.test.sh. No
lavish reference remains in the tree.

Verification: bin/fm-lint.sh clean; fm-bearings-board, fm-bearings-board-render,
fm-watch-triage, fm-secondmate-reconcile, fm-pr-merge, fm-send-remote-delivery,
fm-inactive-reconcile, fm-task-inbox, fm-pi-branch-extension, fm-cursor-primary,
fm-brief, and fm-spawn-pool-base-freshen suites pass. The one fm-bootstrap
failure (test_network_phase_partitions_the_run expecting MISSING: node) is
environmental: this host has /usr/bin/node on the test base path, so removing
the fake node does not hide it; the merge does not touch node detection and the
case passes in CI.
@knowttl
knowttl merged commit a494c9c into main Aug 27, 2026
12 of 13 checks passed
@knowttl
knowttl deleted the fm/fm-upstream-sync-16 branch August 27, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants