sync: merge upstream kunchenguid/firstmate main (9 commits) - #29
Merged
Conversation
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.
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.
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
answerscommand, an argument-count guard in cmd_arm, and a widened usagerange. Both sides kept, combined hunk by hunk:
answersline added.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).
[ "$#" -eq 1 ] || usageguard plus the fork's atelier-axibinary check.
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
answerscommand against a fixture of the publishedqueued-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
lavishadapter id, none of which exist in this fork. Left as-is they wouldhave been dangling: the adapter id in a .adapter state file resolves through
adapter_script() to bin/fm-procevent-.sh, so
lavishwould havepointed at a missing script. Applied the fork's established rename across the
merged-in content:
run_lavish -> run_atelier, the
lavishadapter id, prose Lavish -> Atelier,and the two make_home fixture names.
A repo-wide grep for
lavishnow returns nothing.CLAUDE.md converts from a symlink to a real @AGENTS.md pointer file per 4913723,
absorbed cleanly.
Verification
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:
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.
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.