feat: sync upstream workflow and recovery improvements - #9
Merged
Merged
Conversation
…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.
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
Perform the ordinary daily official-upstream synchronization into fork main for the seven commits e518906..bdae21e that upstream gained while the previous fork-main pull request awaited merge, so the divergence ledger can then be populated. This is a required precondition, not housekeeping: bin/fm-fork-topic.sh line 223 refuses to add any divergence topic unless upstream is already an ancestor of fork main, so registration of the two carried units is blocked until this lands. IMPORTANT DELIVERY REQUIREMENT: merge this pull request with the REGULAR merge method, never squash and never rebase. The two-parent merge is the deliverable; squashing it silently discards the ancestry exactly as it did on pull request 7, which then had to be redone as pull request 8. Captain-set constraints carried forward: follow the fork-main-integration upstream-integration procedure exactly, using an isolated candidate from the private fork integration clone with fm-fork-merge.sh prepare and continue, and validate through the isolated fork-target registration; never force-push or rewrite published history; never allow an upstream integration to happen implicitly as an unreviewed side effect; the captain merges and this pull request still requires explicit captain approval. Two conflicts arose and both needed the two sides combined rather than one side picked. In CONTRIBUTING.md, upstream's CLAUDE.md sentence is taken because upstream replaced the symlink with a real AGENTS.md pointer file and the merged tree genuinely carries that pointer blob-identical to upstream's, so the fork's older symlink wording would now be false, while the fork's following line is kept because pointing at AGENTS.md as the single owner of the shared tracked-material list is exactly the fork divergence, replacing an inline list upstream cannot keep current for a fork that adds its own tracked manifest. In the README command table, the fork's updatefirstmate row is kept because it describes permanent-fork topology validation and separate upstream-integration reporting that only the fork performs, while upstream's stow row is taken because upstream added open-record persistence to that command and the fork should not carry a stale description of shared behavior. No fork behavior was dropped and no upstream guarantee was weakened.
What Changed
bdae21ewith two-parent ancestry preserved, and record the synchronized range in the fork divergence ledger./stow, keep Pi export confirmations visible, and preserve Relay promised-final follow-ups for secondmate-routed work.CLAUDE.mdsymlink with an@AGENTS.mdpointer file, with corresponding CI, documentation, and regression coverage updates.Risk Assessment
🚨 High: Captain, the merge topology and conflict resolutions match the stated intent, but the imported answer-time closure can violate the existing routed-work invariant and needs explicit approval before merge.
Testing
Focused fork-integration and pointer tests passed; actual-target topology, ancestry, ledger, conflict resolutions, and health were verified through a disposable validated fork/upstream topology after the gate checkout’s missing upstream remote prevented an in-place health run, two reviewer-visible transcripts were captured, and the worktree was left clean.
Evidence: Upstream sync validation transcript
Evidence: Two-conflict reconstruction and combined resolutions
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-decision-hold.sh:779- Every captured keyed choice is passed to the unroutedanswerpath, which records(none)and closes the hold. This conflicts with the lifecycle invariant that an answer authorizing follow-up work remains open until dependent work is created andresolverecords the routing; normally that semantic decision happens only after the handler reads the captured answer. Decide whether losing that routed-work audit link is intentional; otherwise persist the answer without closing until the handler selectsresolve,decline, oranswer.bin/fm-procevent.sh:416- Keyed answers are fed only immediately after fresh capture. A crash after durable capture at line 409 but before this call leaves the result eligible for re-announcement, yetreconcileonly republishes pending results and never retries the idempotent answer feed. Replay answer feeding from the pending-result reconciliation boundary so the exact captured-answer/open-hold failure cannot recur after a restart.bin/fm-procevent.sh:713- Decision bindings are removed only by explicitcmd_retire; automatic terminal retirement usesretire_owned_terminal_sourceand bypasses this cleanup. A Lavish Send & End therefore leaves its binding indefinitely, and rearming the same artifact reuses the same source ID and can route later choices to the old origin. Retire the binding at a shared terminal-result lifecycle boundary that still preserves pending-result replay.bin/fm-decision-hold.sh:724-command_unbindremoves throughstate/decision-bindingswithout verifying that the parent directory is a real directory rather than a symlink. A symlinked binding directory makes source retirement delete<source>.originoutside Firstmate state;read_bindingcan similarly read through it. Apply the same parent-directory safety check used bycommand_bindbefore reading or deleting bindings.✅ **Test** - passed
✅ No issues found.
bash tests/fm-fork-main.test.shbash tests/fm-ensure-agents-md.test.shgit merge-base --is-ancestor bdae21e… 18e95c7…andgit merge-base --is-ancestor bdae21e… 86df18c…to verify the guard changes from blocked to eligiblegit rev-list --no-merges --count f1a4af4…..bdae21e…and ordered commit inspectiongit show --remerge-diff 86df18c… -- CONTRIBUTING.md README.mdwith side-specific line and CLAUDE.md blob comparisonsjqvalidation of.upstream_syncs[-1]infork-divergences.jsonbin/fm-fork-status.sh --repo "$PWD" --fork-ref 86df18c… --upstream-ref bdae21e… --facts-only(in-place setup probe; unavailable because the gate checkout has no upstream remote)bin/fm-fork-status.sh --repo <disposable-validated-clone> --fork-ref 86df18c… --upstream-ref bdae21e… --facts-onlygit status --short --untracked-files=alland evidence-file existence checksdocs/configuration.md:393- The imported contract says every secondmate-routed Relay request uses the typed promised-final path, but docs/remote-secondmates.md says remote routes cannot carry delegated public replies, andbriefemits a main-home path with no cross-host transport. Decide whether to scope the promise to local secondmates or add remote result transport; adding transport best preserves upstream’s stated guarantee.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.