Conversation
Starting 40 shells costs about 1.5s under Git Bash against milliseconds on Linux, so several fixed-second budgets in the watcher-lock suite expired mid-race and reported failures that were really the clock. tests/wake-helpers.sh gains FM_TEST_PROC_WAIT_SCALE, 8 on msys/mingw/cygwin and 1 elsewhere, applied inside wait_for_exit so all nine call sites are covered. Raising an upper bound is free on a fast host because each loop breaks the moment the process is gone. bin/fm-watch-arm.sh already carries the same platform split for the same reason. The concurrency case no longer holds the lock for a fixed second. It now barriers until all 40 contenders have finished their attempt, which is what the case actually means. Under Git Bash a backgrounded shell can begin executing well after it is launched, so the old winner exited before the last contender started; a late contender then correctly reclaimed a dead-pid lock and scored as a second winner for behaving exactly as designed. The lock itself needed no change. Instrumented under 40 contenders, exactly one process cleanly created the lock and the apparent second winner had explicitly reclaimed the dead owner's pid through the intended stale-reclaim path. (cherry picked from commit 98d311f)
fm_harness_ancestry_pids walks up to 16 parents to find the harness process
that owns this home's session lock. On a Git Bash (MSYS) host that walk cannot
succeed, because the MSYS process tree is severed from the Windows one: an MSYS
shell launched by a native Windows parent reports ppid 1, ps -W lists claude.exe
but reports ppid 0 for every native process, kill -0 cannot see a Windows pid
because the two are different number spaces, and ps -o is unsupported. A ps
shim alone therefore cannot fix this.
The consequence is not cosmetic. With no ancestry, bin/fm-lock.sh refuses the
session lock and bin/fm-claude-stop-autoarm.sh then exits 0, silently disabling
the whole watcher and turn-end continuity layer. The home looks fine and
supervises nothing.
Adds bin/fm-winproc-lib.sh, a single owner for what the Windows kernel reports
about a given process, and teaches bin/fm-session-lock-lib.sh to consult it
before falling back to the existing POSIX walk. The bridge reports raw process
facts only; the harness decision stays with its existing owner.
Two independent evidence sources, because they fail for unrelated reasons and
the coding guidelines require that no single vendor-emitted string be
load-bearing:
- the harness-exported pid, confirmed against one ps -W row, about 0.24s
- a memoized Get-CimInstance Win32_Process snapshot walked up 16 hops,
about 1.5s
Either source alone carries a positive verdict. With neither, the ancestry
refuses rather than guessing.
Liveness asks Windows first and falls through to the original kill -0 path when
Windows does not describe the pid, because a recorded pid can belong to either
number space: the native resolver writes a Windows pid while an MSYS-side
process writes an MSYS one. Answering only for the Windows space would call a
live MSYS process dead. The fall-through cannot invent a live process, since a
genuinely dead pid fails both checks.
Linux and macOS are unaffected. Every entry point gates on
fm_winproc_available, which probes /proc/$$/winpid, so off Windows the bridge
returns 1 and callers fall through untouched. Capability detection, never
uname. The new wait-budget multiplier is 1 on every non-MSYS host.
tests/fm-winproc-lib.test.sh adds 14 assertions that run on Linux CI rather
than skipping, which is the point, since this decides whether a home may hold
the session lock at all. It drives the two evidence sources apart through the
library's documented seams and asserts each isolation is not vacuous, plus the
inert path, the fail-closed path, and pid-reuse rejection. It also pins the
backslash conversion itself: a no-op conversion shipped during development and
no test noticed, because the harness regex is unanchored and matched the
unconverted string by luck.
tests/fm-session-lock-ancestry.test.sh sets FM_WINPROC_DISABLE=1, because every
case in it simulates a POSIX host behind a fake ps and would otherwise be
answered by the bridge before the fixtures were reached.
Verified on Windows 11, Git Bash MINGW64_NT-10.0-26200, bash 5.3.15:
fm_harness_ancestry_pids went from no output and exit 1 to the harness pid and
exit 0, and bin/fm-session-start.sh reports a lock acquired against a pid that
matches the live claude.exe exactly. The structural walk resolves the same pid
with the exported pid unset, so neither source is doing all the work.
(cherry picked from commit 36a2fec)
Sourcing bin/fm-winproc-lib.sh unconditionally broke three CI shards for reasons that had nothing to do with the cases being run. Eighteen test fixtures build a partial bin/ holding only the scripts under test, so bin/fm-session-lock-lib.sh arrives there without its new sibling, the source fails, and every function the fixture actually wanted is gone. It surfaced as tests/fm-cursor-primary.test.sh, tests/fm-claude-stop-autoarm.test.sh, and tests/fm-pi-watch-extension.test.sh reporting unrelated assertions with a No such file or directory line embedded in the output. The bridge is now sourced only when readable, with fm_winproc_available stubbed to return 1 otherwise. That is the exact gate both call sites already check first, so a home with no bridge behaves like the non-Windows home it resembles: it has no Windows process facts to offer. It cannot mask a broken install on an MSYS host either, because the callers fall through to the POSIX walk, which still refuses with its original "cannot locate harness process in ancestry" diagnostic rather than passing. tests/fm-winproc-lib.test.sh builds that partial layout and requires the library to load and keep answering. Confirmed by mutation: restoring the unconditional source fails the new case with the identical No such file or directory message CI reported. Also corrects an earlier judgement. Two Windows failures were called pre-existing on the evidence that they failed identically with FM_WINPROC_DISABLE=1. That comparison could not distinguish this bug, because a failing source runs before any seam is read. The two remaining Windows failures were re-checked against pristine main and are genuinely pre-existing, but the original reasoning was not sound. (cherry picked from commit e83edb1)
(cherry picked from commit 437bdc5)
(cherry picked from commit dc814be)
(cherry picked from commit 6319bce)
(cherry picked from commit 303dbcb)
(cherry picked from commit 84f6b84)
…file modes (cherry picked from commit 7a20d9a)
(cherry picked from commit 21e0bb6)
One owner for POSIX-vs-native path conversion on Git Bash: cygpath -m for values handed to native binaries, cygpath -u for MSYS-side comparison, identity fallback off MSYS or when cygpath is absent or fails. Tests drive both branches through the FM_PLATFORM_UNAME seam and a fixture cygpath. (cherry picked from commit d874a6a)
GOTMPDIR is exported in the native form so go.exe resolves it; TASK_TMP records stay POSIX. path_is_ancestor_of normalizes both operands so a Windows spelling of the same directory cannot slip past the prefix test, and the secondmate home guard now also refuses drive roots (/c), which canonicalize to /<letter> rather than / on MSYS. (cherry picked from commit ddd7806)
…siblings (cherry picked from commit eabd64f)
The spawn now exports GOTMPDIR in the native form on Windows, so the two suites that pinned the literal /tmp form compute the expectation through fm_path_native instead. Identity off MSYS keeps Linux CI byte-identical. (cherry picked from commit 9d36163)
0.8.2 is the earliest stable release with an official Windows asset; every stable tag through 0.8.0 ships Linux and macOS binaries only. All five SHA-256 pins come from the GitHub release asset digests, the stale ogulcancelik/herdr org moves to herdrdev/herdr, and the CI pin assertion follows. The Windows .zip installs herdr.exe beside its app-local ConPTY runtime, the layout Herdr's own Windows installer validates. Verified end to end on Git Bash: install rc 0, idempotent rerun byte-identical, herdr 0.8.2 protocol 20. Note: 0.8.2 is not yet in the verified real-Herdr backend matrix (docs/herdr-backend.md:4); the matrix decision is still open. (cherry picked from commit 123db8c)
The fleet-state tripwire required a running default session before any lab work, which made the whole live lane unrunnable on a machine that never started herdr. Safety comes from the byte-identical after-comparison, not the starting state: snapshotting a stopped default and requiring it identical afterward also proves the lab never started the default server. New coverage: a stopped default provisions and snapshots running:false, and a default that changed running state hard-fails teardown with evidence retained. Verified live on Windows: 19 assertions now pass across the three real-herdr suites, up from zero, with the default session byte-identical before and after. (cherry picked from commit 76c07e5)
win32 Python has no socket.AF_UNIX, so the attribute lookup raises AttributeError before any OSError can, and both helpers died with a traceback and exit 1 instead of their designed unavailable exit 2. The adapter already discarded the stderr and normalized the failure, so observable behavior is unchanged; the helpers now keep their own contract. Verified on Windows: both exit 2 cleanly where they crashed before; eventwait smoke unchanged at 1 ok / 1 not-ok. (cherry picked from commit 1982d25)
…o the herdr lock The probe moves from fm-pr-lib.sh into fm-platform-lib.sh as fm_platform_fs_honors_modes (env override renamed FM_PR_MODES_HONORED -> FM_FS_MODES_HONORED), fm-pr-lib delegates, and the herdr presentation lock namespace keeps its directory, symlink, and owning-uid checks always while requiring mode 700 only where the filesystem can store a mode. On a noacl Git Bash mount mkdir -m 700 is a silent no-op and the namespace reads 755 forever, which made the lock permanently unacquirable. Note: on Windows the lock still fails earlier today - herdr reports a native-form socket path that the socket canonicalizer refuses - so this lands as the second of two sequential fixes. (cherry picked from commit 74605c9)
Herdr on Windows reports native socket paths (C:\Users\...\herdr.sock), which the canonicalizer's absolute-path test read as relative and refused, so the presentation session lock could never be acquired there. The incoming socket is folded through fm_path_posix first, exactly because this function is the single identity owner: C:\x, C:/x, and /c/x must all collapse to one lock identity; off MSYS the fold is the identity function. With this and the mode-probe change together, the live smoke lane's three presentation-lock warnings drop to zero, the lock namespace is created, and every lock taken is released. New seam-driven regression coverage collapses all three spellings and pins the off-MSYS refusal of drive-lettered paths. (cherry picked from commit f72503f)
herdr's shell_pid is a native Windows pid on Windows, which MSYS ps cannot address (no -o at all, and its PID column carries MSYS pids - a naive shim would cross namespaces). The idle-shell proof's three ps reads become one fm_winproc_pid_census snapshot (both counts from one moment, so they cannot straddle an exit), the recognized-shell checks share one normalizer that also strips the .exe herdr reports for Git Bash panes, and signaling routes through /usr/bin/kill -W, which is blind to processes outside the MSYS runtime and so can never signal a native process (verified: HUP ends a real idle pane shell, KILL escalates past a HUP-ignoring one, and a foreign native pid reads as No such process). stat= has no Windows source and is relaxed there with the reasoning in place; the POSIX path is byte-identical. Verified live: the idle-shell proof passes and the pane-death close ends a pane where it previously refused, with zero lock warnings and the default session untouched. (cherry picked from commit f6b95ad)
Herdr 0.8.2 protocol 20 is the earliest stable release carrying a Windows x86_64 asset, so the installer pins it on every platform rather than keeping a separate Windows pin. Record that choice, add 0.8.2 to the verified version list and the floor table, and note that its row is the one entry not produced by the macOS aarch64 sweep. Add a Windows x86_64 verification section covering the measured live lane and the four adapter paths that carry the platform's differences: the native socket-path fold, the probe-and-accept lock mode gate, the bash.exe shell normalizer, and the Windows-pid-space process reads. Record the one open blocker plainly. foreground_cwd is always null on Windows and a pane's own cwd freezes at creation, so the worktree-discovery poll reads nothing and a crewmate or scout spawn refuses instead of recording an unverified directory. The guard is behaving correctly, but an ordinary Herdr spawn cannot complete on this platform until the directory is read from inside the pane rather than off the terminal. (cherry picked from commit c6d25d6)
Treehouse already publishes Windows assets at the pinned v2.0.1, so this needed no version bump, unlike the Herdr installer port. The Windows arm differs from the existing platforms in two ways only: the asset is a .zip rather than a .tar.gz, and the binary inside it is treehouse.exe. Add ARCHIVE_FORMAT and BINARY so the extract, install, and version-check steps stay one code path across every platform instead of growing a parallel Windows branch. Check for unzip before spending the download, matching the Herdr installer's ordering. The sha256 pin comes from the GitHub API digest field for treehouse-v2.0.1-windows-amd64.zip. Verified live on Windows 11 x86_64 under Git Bash: the install completes with a matching checksum and the installed binary reports the exact pin v2.0.1. Linux CI is unaffected; it never selects the new arm. (cherry picked from commit 3e784d2)
Spawn discovered a crewmate or scout worktree by sending `treehouse get` to the endpoint and polling the pane's working directory until it left the project. That required a backend able to report a live foreground path. Herdr on Windows cannot: foreground_cwd is always null and a Git Bash pane's own cwd freezes at creation, so no poll ever observed the pane moving and every crewmate and scout spawn refused after sixty seconds. The refusal was correct, but the platform could not spawn at all. Acquire the worktree in fm-spawn.sh's own shell instead. `treehouse get --lease` is non-interactive, opens no subshell, and prints only the worktree's absolute path, so the path is known before the endpoint is told anything. The endpoint is then sent a plain cd, and the pane's own position is confirmed best-effort: a backend that reports a path must agree, while an empty read means the platform cannot answer and the spawn proceeds. The ship brief's own isolation assertion is the agent-side backstop there. validate_spawn_worktree is unchanged and still gates every spawn. It reads the filesystem rather than the terminal, so it is the isolation guarantee on every platform regardless of where the path came from. A lease is durable, so every failure between the lease and the published task record hands it back through the existing exit trap or a pool slot is burned permanently. The rollback disarms once state/<id>.meta names the worktree, matching the boundary the Orca worktree cleanup already used: returning a worktree a live record still points at would hard-reset it and hand it to another task. On Windows treehouse prints a native backslashed path, measured live, which no POSIX comparison here can use and which teardown could not match against its record. Fold it through fm_path_posix at the single point of capture, as the Herdr canonical socket path is folded. The regression covering that runs on every platform through the library's own uname and cygpath seams, and fails without the fold. Remaining changes are the test stub every spawn-shaped fixture now needs, and comment and documentation corrections wherever the old mechanism was described. (cherry picked from commit 0b11112)
Windows support was spread across backend guides and a verification record, so an operator had no single place that answers "what do I need, and what is different here". Collect that into one operator-current page and route to it from the README and the audience inventory. The page leads with the symbolic-link setting because it is the one whose absence fails silently: without it every lock quietly fails to be taken, supervision never starts, and a checkpoint reports a quiet fleet it never actually observed. It also covers the line-ending trap a fresh clone hits, the pinned installers, the Herdr pane shell requirement, the current platform limits and what each degrades to, and troubleshooting keyed by the symptom rather than the cause. The backend guides keep ownership of their own setup and safety boundaries, and the verification record keeps the measured evidence; this page carries only what Windows changes and points at those owners. (cherry picked from commit 3ebb6a3)
The previous commit carried an unintended one-line edit: an `expect_code` failure message in the trace-context spawn test had the captured spawn output appended to it while a suspected regression was being chased. That regression turned out not to exist, and the edit was never meant to ship. It changed only what a failure prints, never whether the case passes, so this restores the original message and nothing else. Fixed forward rather than by amending, because the carrying commit is already pushed. (cherry picked from commit 846b25a)
The page assumed a reader who already had firstmate running and only needed the platform's differences. A first-time Windows operator has no entry point before the symlink setting, so add three sections ahead of it: what firstmate is, how to install it here, and how to run it. Each new step points at the section that owns its detail rather than restating it, and the context section points at the README and the architecture doc. The install section also carries a copy-paste prompt for handing the setup to Claude Code. (cherry picked from commit 013f428)
The context section said there is no application to install. Firstmate itself has no installer, but Herdr, Treehouse, ShellCheck, and actionlint are all real installs the same page then walks you through, so the claim read as false to anyone about to do that work. Keep the accurate half - the clone is the distro - and say plainly that the separately installed tools are what the install section is for. (cherry picked from commit 58061d3)
The install step and the Claude Code prompt both told a reader to clone the upstream repository. Upstream carries no Windows support at all - not even this page - so following that step produces a checkout where nothing else on the page applies, with no error to explain why. Point both at the windows branch of the fork, and say plainly that upstream is not a substitute. (cherry picked from commit 638c39f)
A Herdr pane opens a non-login shell on Windows, so /etc/profile never runs and the pane's PATH carries Git for Windows' `bin` directory, which holds only bash, sh, and git. /usr/bin is absent there, so `env`, every `#!/usr/bin/env bash` shebang, and the harness binary itself are unresolvable: the launch line died at the prompt while the spawn still reported success, so no agent ever started and nothing said so. Export firstmate's own PATH into the pane ahead of the launch command, through the same pre-launch channel GOTMPDIR already uses, keeping the pane's own entries behind ours. Same root cause as the CLAUDE_CONFIG_DIR forward beside it: the backend daemon does not inherit firstmate's environment. Proven live on Windows: a real Claude Code agent now starts from `fm-spawn.sh --scout --backend herdr --harness claude` in an isolated rig and guarded lab. The new tests fail on pre-fix code and pass on fixed code. (cherry picked from commit 1171e51)
… path The Herdr client is a native Windows program, so under MSYS Git Bash rewrites any argument it reads as a POSIX path before the client ever sees it. Every call that sends literal input to a pane went through that conversion, so a harness slash command such as `/exit` arrived as `C:/Program Files/Git/exit`. Measured in a live pane: `fm-control.sh <id> exit` delivered that mangled text, the agent never stopped, and the run ended at `exit=unconfirmed`. Route the three literal-payload calls - `pane run`, `pane send-text`, and `pane send-keys` - through a wrapper that suppresses argument conversion on MSYS only. Every other herdr call keeps converting, because those do pass real paths a native client must receive in native form. Verified live on Windows 11 with herdr 0.8.2 and a real Claude agent: `fm-control.sh exitproof-w1 exit` now reports `stopped`, where the same command before this change reported `exit=unconfirmed; the agent did not stop within 30s`. Three portable regressions drive fm-platform-lib's FM_PLATFORM_UNAME seam and read the fake client's opt-in environment log; disabling the MSYS branch makes the first of them fail. (cherry picked from commit 4c275f5)
…line reads The Windows jq build writes its output in text mode, so every line it emits ends CRLF. A single-value capture survives untouched, because command substitution strips the whole trailing CRLF, but a multi-line capture keeps an interior CR on every line but the last. Those CRs then ride inside workspace, tab, and pane ids and into text joined from them. Two measured consequences on Windows, both caught by this suite: the ambiguous workspace refusal named its candidates as `w1<CR> w7`, and create_task read every husk tab id but the last with a trailing CR, found no pane for it, and refused with "already exists" instead of replacing it. Add fm_backend_herdr_strip_cr and route the five multi-line jq reads through it. It is a plain filter rather than an MSYS branch so the same bytes come back on every platform, which is what lets one portable regression cover it. The dup_tabs read strips in a second step instead of extending its pipeline, because jq's exit status is load-bearing there and the filter's own success would hide a parse failure. The suite goes from 22 passing to 67 on Windows. It exits at the first failure, so the cases after the ambiguity one had never run on this platform; the remaining failure at the projection close path reproduces at HEAD without this change and is untouched here. (cherry picked from commit dd78578)
Collaborator
Author
|
Closing: wrong shape. This branch mixed the Windows port with six unrelated fork-local features. The branch has been rebuilt to carry only the Windows work, and each feature now sits on its own branch cut from clean main. The pre-split state is preserved on archive/windows-pre-split. |
notno
added a commit
that referenced
this pull request
Sep 8, 2026
…r replies (#2) * feat(procevent): add a Telegram process-event adapter An operator reply from a phone now wakes firstmate through the existing process-event runner instead of sitting unseen in a chat. bin/fm-procevent-telegram.sh long-polls getUpdates from an offset one past the recorded cursor and returns the first non-empty batch as a captured result. The poll only reads the cursor: the handler advances it after fully handling the messages, so a crash never advances past unhandled replies. A restart before that advance re-captures the same updates, so the header states plainly that handlers must treat any update id at or below the cursor as already seen. The bot token is read from a private file and passed to curl through a stdin config, so it never reaches the argv, stdout, or stderr. Transient transport and server errors retry with bounded backoff; HTTP 401 and 403 exit non-zero so the runner surfaces a rejected token rather than spinning. A message stream never self-terminates, so terminal always keeps the source armed and retirement stays an explicit operator action. * no-mistakes(review): add telegram cursor ack, webhook/rate-limit and batch guards * no-mistakes(review): serialize telegram cursor ack, clarify 409 and handler steps * no-mistakes(review): document ack-before-handled order, dedupe ack teardown * no-mistakes(document): document telegram adapter env knobs and toolbelt row * no-mistakes(lint): fix shellcheck SC1007 and SC2012 in telegram adapter * no-mistakes(review): gate duplicate telegram emissions, keep failed child stderr * no-mistakes(review): redirect child stderr to file, record telegram handoff after delivery * no-mistakes(document): document kept failure diagnostic for process-event runner * no-mistakes(document): anchor procevent help extraction to header end marker * no-mistakes(review): drop emitted-marker dedupe for at-least-once polls, anchor help extraction * no-mistakes(document): list telegram ack ordering in procevent skill description * no-mistakes(document): split telegram ack skill guidance to one sentence per line (cherry picked from commit 457c4d8)
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.
Scope
This branch carries more than the Windows port.
mainwas reset to match upstream, sowindowsis now the only branch holding this fork's own work. Landing it brings two distinct bodies of change:Review accordingly, or split the branch if only the port is wanted right now.
The Windows port
Firstmate previously assumed a POSIX host. Three classes of Windows-specific breakage are fixed here, each measured against a real Herdr session rather than inferred:
/etc/profilenever runs and/usr/binis absent. Every#!/usr/bin/env bashshebang and the harness binary itself become unresolvable: the launch line dies at the prompt while the spawn still reports success. Firstmate now ships its own PATH in ahead of the launch line, keeping the pane's own entries behind it./exitslash command arrived in the pane asC:/Program Files/Git/exit; the agent never stopped and the send still reported success. Conversion is now suppressed for exactly the calls whose payload is literal pane input, and left in place for calls that genuinely pass paths.jqline endings. The Windowsjqbuild writes in text mode. A single-value capture survives untouched because command substitution strips the whole trailing CRLF, but a multi-line capture keeps a carriage return inside every line but the last, and those then ride inside workspace, tab, and pane identifiers. Multi-line reads are now filtered.Earlier commits on the branch cover path-form conversion, Windows-pid-space pane resolution, socket identity, mode-bit capability probing, and Windows installers for the Herdr, treehouse, and lint toolchains.
docs/windows.mdis the operator manual for all of it.Upstream merge
origin/mainis merged in (11 commits). Two files conflicted:AGENTS.md- kept the fork's trimmed section 2 layout block from feat(bin): resolve harness identity from Windows process facts #1. It is a deliberate change to pointers, not stale text, anddocs/configuration.mdremains the owner of every entry it no longer lists.bin/fm-brief.sh- kept the fork's keyedneeds-decisioninstructions, which are a superset of upstream's and consistent with the surrounding un-conflicted key language.Three clean auto-merges were reviewed semantically rather than trusted:
52d20f1(ask-user decoupled from yolo) and the Windows PATH fix both land intact inbin/fm-spawn.sh;5b6d0fb(GitLab merge requests) merges without interaction.Verification
Run on Windows 11, Git Bash, inside a real Herdr 0.8.2 session.
bin/fm-lint.sh- clean (ShellCheck 0.11.0, actionlint 1.7.12, 3 workflows valid).bin/fm-doc-audience-check.sh- ok, 74 surfaces, 277 local links.tests/fm-brief.test.sh- all pass.tests/fm-backend-herdr.test.sh- 67 pass, unchanged by the merge.Known issue
test_projection_close_emptying_after_focus_uses_pane_death_without_movefails on Windows. It was verified pre-existing against an untouched checkout at this branch's base, is unrelated to the carriage-return class, and is not addressed here. The suite stops at the first failure, so further Windows failures may sit behind it.Note for reviewers
One upstream commit,
266fdb9, adds its ownuname-conditional platform branch inbin/fm-busy-event.shwhile this branch addsbin/fm-platform-lib.sh. The two layers do not conflict today but are growing independently and are worth consolidating before they diverge further.