Skip to content

sync: merge upstream kunchenguid/firstmate main (9 commits) - #29

Merged
knowttl merged 10 commits into
mainfrom
fm/fm-upstream-sync-11
Aug 17, 2026
Merged

sync: merge upstream kunchenguid/firstmate main (9 commits)#29
knowttl merged 10 commits into
mainfrom
fm/fm-upstream-sync-11

Conversation

@knowttl

@knowttl knowttl commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Absorbs the 9 commits this fork was behind, preserving the fork's own 56
commits. Merge base 1238402.

Absorbed:

No feature loss. Upstream's batch is fixes, one /stow feature, CI hardening,
a gitignore entry, and the keyed-answer path; nothing upstream deletes or
replaces fork work, and no conflict took an incompatible direction.

Conflict resolutions

bin/fm-procevent-atelier.sh (upstream bin/fm-procevent-lavish.sh; rename-detected)
The fork's only divergence in this file is the settled lavish-axi -> atelier-axi
rebrand (df367d7) plus the 0.3.3 published-interface note. Upstream added the
answers command, an argument-count guard in cmd_arm, and a widened usage
range. Both sides kept, combined hunk by hunk:

  • usage list: fork's atelier-* command names, with upstream's answers line added.
  • header: upstream's keyed-answer contract paragraphs, retranslated Lavish ->
    Atelier, followed by the fork's 0.3.3 / --full interface note (upstream's
    0.1.45 / lavish-axi line dropped as superseded by the fork's newer evidence).
  • cmd_arm: upstream's [ "$#" -eq 1 ] || usage guard plus the fork's atelier-axi
    binary check.
  • usage sed range recomputed to 2,49 for the merged header (base 35 + upstream 12
    • fork 2) and confirmed against real --help output.
      Upstream's cmd_answers body auto-merged unchanged.

docs/verification/process-event-sources.md
Version line conflicted. Kept the fork's line (2026-08-11, Linux WSL2 kernel
6.18, atelier-axi 0.3.3), which is this fork's current platform and version.
Upstream's replacement claim - the keyed-answer feed verified 2026-07-31 on
macOS against lavish-axi 0.1.45 - is not a fact this fork can assert, so it was
not copied over. Replaced with an accurate statement of what is actually proven
here: the portable regression in tests/fm-decision-hold-lifecycle.test.sh drives
the adapter's answers command against a fixture of the published
queued-feedback response shape, and the response shape itself remains an
unrefreshed vendor fact because no live atelier-axi answer submission has been
observed on this platform.

Dangling-reference cleanup (auto-merged content, no conflict markers)
Upstream's new content referenced bin/fm-procevent-lavish.sh, lavish-axi, and a
lavish adapter id, none of which exist in this fork. Left as-is they would
have been dangling: the adapter id in a .adapter state file resolves through
adapter_script() to bin/fm-procevent-.sh, so lavish would have
pointed at a missing script. Applied the fork's established rename across the
merged-in content:

  • docs/decision-hold-lifecycle.md: fm-procevent-lavish.sh -> fm-procevent-atelier.sh
  • tests/fm-decision-hold-lifecycle.test.sh: script path, lavish-axi stub,
    run_lavish -> run_atelier, the lavish adapter id, prose Lavish -> Atelier,
    and the two make_home fixture names.
    A repo-wide grep for lavish now returns nothing.

CLAUDE.md converts from a symlink to a real @AGENTS.md pointer file per 4913723,
absorbed cleanly.

Verification

  • bin/fm-lint.sh: pass (ShellCheck 0.11.0, pinned).
  • bin/fm-doc-audience-check.sh: pass (70 surfaces, 257 local links).
  • bin/fm-test-run.sh over the touched subsystems - decision-hold-lifecycle,
    procevent, procevent-when, ensure-agents-md, backlog-handoff,
    remote-backlog-handoff, send-resolve-key, send-strict, send-settle,
    send-secondmate-marker, x-mode, calm-pi-extension, test-run: 11 of 13 pass,
    including upstream's new keyed-answer cases exercising the renamed adapter.

Two failures, both environmental, neither merge-caused. Each was reproduced
identically on a pristine upstream/main tree containing no fork content:

  • tests/fm-test-run.test.sh: two independent local gaps. Under this machine's
    en_US.UTF-8 collation the test's own comm calls at lines 28-29 (unlike line
    362) run without LC_ALL=C and abort with "file 2 is not in sorted order";
    forcing LC_ALL=C clears that and exposes the second gap, "ruby is required to
    parse .github/workflows/ci.yml as YAML" - ruby is not installed here.
  • tests/fm-calm-pi-extension.test.sh: a live tmux end-to-end case against the
    real Pi agent. @earendil-works/pi-coding-agent is absent from npm root -g,
    so most cases self-skip and the loaded_on follow-up case fails.

kunchenguid and others added 10 commits August 13, 2026 23:30
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
…d mate (kunchenguid#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
…verdicts (kunchenguid#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
…nguid#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to reset" - wording that implied a
record-correctness guarantee stow does not make. A shipped PR with no
backlog item, a queued umbrella whose phases had merged, and four decision
holds left open after their answers shipped all survived repeated stows.

Add a bounded pass that files record state from the same volatile input the
rest of stow already uses: the open threads in context, minutes before the
reset destroys them. It creates a record for an unfiled thread and corrects
one the session knows is wrong, through the owning path, and states its
boundary as part of the contract - it never enumerates the backlog, lists
holds, or queries a forge, because it cannot be a reconciliation and must
not be read as one.

Correct the wording in AGENTS.md and the completion receipt so reset-safe
means what it actually guarantees: nothing this session knew was lost.

* no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi

* no-mistakes(document): note /stow open-record persistence in README command catalog

* refactor(stow): state open-record persistence as principle, not procedure

The first version enumerated triggers, named commands, and prescribed an
ordered procedure. That is too rigid for an agent skill: it invites literal
execution of a checklist instead of judgment, and every enumerated example
is a way for the guidance to go stale.

Reduce it to the intent - before a reset, the important open work you are
holding in context must end up durably recorded rather than dying with the
session, filing what is unfiled and correcting what is stale - and let the
agent judge importance, the record, and the owning write path.

Keep the scope bound, since it is a decided contract and not a mechanic:
this covers the open work the session is holding, never a reconciliation of
durable records against repository or forge reality. The wording
corrections in AGENTS.md and the completion receipt are unchanged.
…eyed-answer path (kunchenguid#2490)

* fix(decisions): close captain holds at answer time

Firstmate had two "a decision is open" ledgers with asymmetric closing
mechanics. The live status-log ledger closes atomically at answer time,
because bin/fm-send.sh --resolve-key makes answering a decision be the
act that closes it. The durable backlog hold ledger had no such coupling:
answering and recording were two separate acts, and only the first was
forced by the workflow.

That asymmetry lost four real captain decisions. Their answers were
captured durably to disk, keyed character for character by the hold
decision keys, acknowledged, and even implemented and shipped, yet the
holds stayed open for two days and the captain was asked to re-answer
decisions already on his own disk.

Give the hold ledger the same answer-time-closure property:

- bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's
  counterpart to --resolve-key. It shares one unrouted close
  implementation with `decline`, so it carries every existing guard - the
  captain decision file, the active-hold requirement, retry identity, and
  the refusal to release still-routed work - and differs only in the
  resolution mode it records. `decline` keeps its stronger meaning that
  the answer routes no follow-up work at all.
- bin/fm-procevent-lavish.sh wires the channel that actually carried the
  lost answers. `arm --decisions-origin` binds a deck to the origin whose
  holds it carries, `answers` reads the structured choices out of a
  captured poll result, `close-decisions` maps each key to its hold and
  closes it through the command above, and `autohandle` lets the runner
  apply that at capture time.

Safety is preserved rather than traded away. Only rows tagged `choice`
are read, so freeform captain prose cannot forge a decision key. Closure
is confined to the one bound origin. The decision text is a pure function
of the captured result, so a replayed capture is idempotent. A hold that
is absent, already closed, or still blocking routed work is skipped and
left for `resolve`, never forced. A deck armed without the binding
touches no hold at all. And autohandle deliberately never reports full
handling, because recording an answer is transcription while acting on it
is firstmate's judgement - so the check wake still reaches the handler.

fm-send --resolve-key is untouched.

* no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory

* refactor(decisions): make keyed-answer closure one general capability

The previous pass gave holds answer-time closure but built it as bespoke
Lavish wiring: the review adapter carried the source-to-origin binding,
mapped keys to hold identities, wrote decision records, decided what to
skip, and closed holds itself. That treated a review deck as a special
decision source. It is not - it is an ephemeral discussion format that
happens to carry answers.

Collapse it into ONE general capability with one owner.

bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its
matching hold":
- `answers <origin> --source <provenance>` is the channel-agnostic
  intake. It reads key/answer/label lines on stdin, maps each key to its
  hold, and closes it through the same `answer` path, so every guard
  applies identically whatever channel the answer came from. --source is
  provenance recorded in the decision, never a behavior switch; there is
  no per-channel branch and no knowledge of chat, decks, or transports.
- `bind`/`unbind`/`binding` own the source-to-origin binding for any
  channel whose answers arrive detached from their origin.

Every channel is now an ordinary caller that only turns what it received
into keyed lines:
- bin/fm-send.sh (chat) feeds the intake for a key that names an active
  hold. This also fixes a real gap: once `complete` transfers a decision
  to its hold it closes the live status copy, so --resolve-key alone
  could never answer a transferred decision.
- bin/fm-procevent.sh feeds it generically. A bound source's captured
  result goes to `<adapter> answers <result-file>` and whatever that
  prints is piped into the intake. The runner names no adapter, parses
  no result, and carries no decision rule, so any future adapter with an
  `answers` command works with no change here.
- bin/fm-procevent-lavish.sh keeps only `answers`, which reports the
  structured choices a review captured and stops. It maps nothing to a
  hold and closes nothing; it lost ~160 lines of decision logic.

Feeding is independent of handling, so it never acknowledges a result
and never suppresses a wake - recording an answer is transcription,
acting on it stays firstmate's judgement.

The regression that proves closure now drives a FIXTURE adapter that is
not the review adapter, so what is proven is that any bound channel
reaches the intake rather than that one channel is wired specially. A
new regression drives the real fm-send over a stubbed transport for the
chat side. Every prior guarantee still holds, and fm-send's status-log
behavior is unchanged.

* no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression
…mlink (kunchenguid#2512)

A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md.
The installer now creates and migrates to a recoverable two-line pointer file.
Absorbs the 9 commits this fork was behind, preserving the fork's own 56
commits. Merge base 1238402.

Absorbed:
- bdae21e fix(ci): keep CLAUDE.md pointer check valid (kunchenguid#2515)
- 4913723 fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (kunchenguid#2512)
- 362c508 fix(decisions): close decision holds at answer time via one general keyed-answer path (kunchenguid#2490)
- e518906 feat(stow): add open-record persistence to /stow before reset (kunchenguid#2488)
- ef35d79 fix(calm): keep Pi's export confirmation visible (kunchenguid#2461)
- 196fb65 docs(skills): add remote-secondmate recovery hint for false-negative verdicts (kunchenguid#2456)
- 7a3259e fix: keep the public promise reachable when work is routed to a second mate (kunchenguid#2457)
- f1a4af4 fix(ci): fail hung Herdr behavior runs in 20 minutes (kunchenguid#2413)
- 6789876 chore: ignore scratchpad/ at the repo root (kunchenguid#2359)

No feature loss. Upstream's batch is fixes, one /stow feature, CI hardening,
a gitignore entry, and the keyed-answer path; nothing upstream deletes or
replaces fork work, and no conflict took an incompatible direction.

Conflict resolutions
--------------------

bin/fm-procevent-atelier.sh (upstream bin/fm-procevent-lavish.sh; rename-detected)
The fork's only divergence in this file is the settled lavish-axi -> atelier-axi
rebrand (df367d7) plus the 0.3.3 published-interface note. Upstream added the
`answers` command, an argument-count guard in cmd_arm, and a widened usage
range. Both sides kept, combined hunk by hunk:
- usage list: fork's atelier-* command names, with upstream's `answers` line added.
- header: upstream's keyed-answer contract paragraphs, retranslated Lavish ->
  Atelier, followed by the fork's 0.3.3 / --full interface note (upstream's
  0.1.45 / lavish-axi line dropped as superseded by the fork's newer evidence).
- cmd_arm: upstream's `[ "$#" -eq 1 ] || usage` guard plus the fork's atelier-axi
  binary check.
- usage sed range recomputed to 2,49 for the merged header (base 35 + upstream 12
  + fork 2) and confirmed against real --help output.
Upstream's cmd_answers body auto-merged unchanged.

docs/verification/process-event-sources.md
Version line conflicted. Kept the fork's line (2026-08-11, Linux WSL2 kernel
6.18, atelier-axi 0.3.3), which is this fork's current platform and version.
Upstream's replacement claim - the keyed-answer feed verified 2026-07-31 on
macOS against lavish-axi 0.1.45 - is not a fact this fork can assert, so it was
not copied over. Replaced with an accurate statement of what is actually proven
here: the portable regression in tests/fm-decision-hold-lifecycle.test.sh drives
the adapter's `answers` command against a fixture of the published
queued-feedback response shape, and the response shape itself remains an
unrefreshed vendor fact because no live atelier-axi answer submission has been
observed on this platform.

Dangling-reference cleanup (auto-merged content, no conflict markers)
Upstream's new content referenced bin/fm-procevent-lavish.sh, lavish-axi, and a
`lavish` adapter id, none of which exist in this fork. Left as-is they would
have been dangling: the adapter id in a .adapter state file resolves through
adapter_script() to bin/fm-procevent-<adapter>.sh, so `lavish` would have
pointed at a missing script. Applied the fork's established rename across the
merged-in content:
- docs/decision-hold-lifecycle.md: fm-procevent-lavish.sh -> fm-procevent-atelier.sh
- tests/fm-decision-hold-lifecycle.test.sh: script path, lavish-axi stub,
  run_lavish -> run_atelier, the `lavish` adapter id, prose Lavish -> Atelier,
  and the two make_home fixture names.
A repo-wide grep for `lavish` now returns nothing.

CLAUDE.md converts from a symlink to a real @AGENTS.md pointer file per 4913723,
absorbed cleanly.

Verification
------------
- bin/fm-lint.sh: pass (ShellCheck 0.11.0, pinned).
- bin/fm-doc-audience-check.sh: pass (70 surfaces, 257 local links).
- bin/fm-test-run.sh over the touched subsystems - decision-hold-lifecycle,
  procevent, procevent-when, ensure-agents-md, backlog-handoff,
  remote-backlog-handoff, send-resolve-key, send-strict, send-settle,
  send-secondmate-marker, x-mode, calm-pi-extension, test-run: 11 of 13 pass,
  including upstream's new keyed-answer cases exercising the renamed adapter.

Two failures, both environmental, neither merge-caused. Each was reproduced
identically on a pristine upstream/main tree containing no fork content:
- tests/fm-test-run.test.sh: two independent local gaps. Under this machine's
  en_US.UTF-8 collation the test's own comm calls at lines 28-29 (unlike line
  362) run without LC_ALL=C and abort with "file 2 is not in sorted order";
  forcing LC_ALL=C clears that and exposes the second gap, "ruby is required to
  parse .github/workflows/ci.yml as YAML" - ruby is not installed here.
- tests/fm-calm-pi-extension.test.sh: a live tmux end-to-end case against the
  real Pi agent. @earendil-works/pi-coding-agent is absent from `npm root -g`,
  so most cases self-skip and the loaded_on follow-up case fails.
@knowttl
knowttl merged commit 05d81a9 into main Aug 17, 2026
23 of 25 checks passed
@knowttl
knowttl deleted the fm/fm-upstream-sync-11 branch August 17, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants