Sync upstream main (11 commits), and fix the Windows ancestry walk it exposed - #3
Merged
Merged
Conversation
…nguid#1171) * fix(grok): adapt Stop continuation to runtime capability * no-mistakes(review): Reject ambiguous Grok Stop payloads * no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions * no-mistakes(review): Enforce exact tmux cleanup selectors * no-mistakes(test): Fix historical tmux fixture and validate Grok Stop * no-mistakes: apply CI fixes
* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2 fm-brief.sh built each Definition-of-done block and the not-enabled Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS /bin/bash) the lexer scans for the command substitution's closing `)` textually and tracks quote state through the heredoc body, so a single apostrophe, unbalanced quote, or unbalanced paren in that prose breaks parsing of the whole script. Every ship-brief scaffold (no-mistakes, direct-PR, local-only) failed with `unexpected EOF while looking for matching )`. Bash 4+ parses it fine, so the breakage stayed invisible everywhere except stock macOS. Replace all four command-substitution heredocs with `IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)` wrapper and the entire defect class regardless of future prose, and preserves the variable expansion the direct-PR and local-only bodies need. `read` keeps the heredoc's trailing newline that `$(...)` used to strip, so trim one newline to keep every generated brief byte-identical to prior output. Guard the structure, not one historical phrase: a new test rejects any heredoc nested in a command substitution anywhere in fm-brief.sh, where the old assertion pinned a single apostrophe phrase and so missed the reintroduction. Extend the stock-macOS Bash CI job from parsing one script to the whole maintained shell surface (bin/*.sh, bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file set so parse scope and lint scope cannot drift apart. * no-mistakes(review): Captain: harden Bash structure and inventory guards * no-mistakes(document): Align stock macOS Bash contributor checks * no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings
* fix(test): pin teardown tmux baseline to historical kill selectors merge-base HEAD main collapses to HEAD after the exact-selector change lands on the default branch, so the old teardown fixture was accidentally exercising current exact targets. Resolve a content-historical permissive tmux adapter from first-parent history and force that post-squash topology inside the conformance case so main and feature branches keep the same old-vs-new contract. * no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings
…id#1197) Cut the runtime skill to the compact pace-aware selection procedure plus minimum owner pointers. Keep every distinct decision rule and move expanded acceptance scenarios to deterministic fixture ownership assertions. Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).
…1219) * Inherit config/backend into secondmate homes with deliberate-override preservation Add backend to the shared inheritable config allowlist so launch, locked bootstrap, and config-push converge a primary pin into secondmate homes as each home local future-spawn default. Track last-inherited bytes in a private state provenance marker so deliberate per-home overrides survive present and absent primary convergence, keep --backend and FM_BACKEND stronger, and extend the existing inheritance tests plus docs and skill claims. * no-mistakes(review): Preserve equal unprovenanced backend overrides * no-mistakes(review): Preserve symlink overrides and verify spawn precedence * no-mistakes(review): Snapshot backend inheritance for consistent provenance * no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence * no-mistakes(document): Document inherited backend override preservation * fix: restore primary-authoritative backend inheritance after document regression The document step reintroduced provenance and deliberate per-home override semantics after review had simplified config/backend to plain primary-authoritative allowlist membership. Restore the primary-always-wins path: present overwrites, absent removes, no provenance marker, and docs/tests match that contract. * no-mistakes(review): Add divergent backend precedence regression fixtures * no-mistakes(document): Document backend inheritance contract
* fix(pi): remove Calm's exclusive Pi upper-version ceiling
tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.
Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.
* no-mistakes(review): Probe missing Calm adapter exports safely
* no-mistakes(document): Document Calm's unbounded Pi compatibility
…enguid#1204) * fix(guard): allow session-local todo tools in the primary The delegation-shape guard denied TaskCreate and TaskUpdate because their normalized names contain the `task` stem. Those tools write only the harness's session-local todo list, which has no executor: it spawns no agent, allocates no worktree, registers no schedule, and starts nothing that outlives the session. That is not the unaccounted work the guard exists to stop, so the stem match was a false positive, and the deny text told the primary to run bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry. Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only observe or stop existing work. Both lists stay exact-name so neither can widen by substring. Tests cover the two allowed names and six near-miss names that a substring or shortened-stem widening would release; both mutations were watched red. * no-mistakes(review): drop session-local todo tools from recommended deny list * no-mistakes: apply CI fixes
…claude pid (kunchenguid#1206) * fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid fm_harness_ancestry_pid() previously returned the first ancestor process whose command matched a verified harness name. Claude Code's Stop hook fires as a bg-spare worker several levels below the session's actual lock-owning claude process (hook shell -> claude bg-spare -> claude bg-pty-host -> claude -> claude(lock)), so the first match was the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self() then never matched state/.lock, and the Claude Stop auto-arm silently treated its own primary session as an unrelated live owner and never armed the watcher. The walk now keeps going past a claude-named match, looking for a still more ancestral claude-named match, and stops the instant a non-match follows an already-found match (bounding it to a contiguous run rather than the literal ancestry top, so an unrelated claude-named process further up the real process tree is never mistaken for part of this session's own nested chain). Every other harness keeps the original first-match-wins behavior, since e.g. Pi's shared signed-wrapper ancestry actually holds the session at the inner engine pid, not an outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper bg-spare chain. * no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim * no-mistakes: apply CI fixes
* fix: confirm watcher startup on MSYS * no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test * no-mistakes(review): validate OpenCode ready timeout, make uname cache internal
…d#1195) * fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates Crewmate panes are created by a long-lived tmux/herdr daemon that does not inherit firstmate's current environment. When firstmate runs under a non-default CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare `claude` in the crewmate pane fell back to the default ~/.claude store and launched unauthenticated, blocking the crewmate before it could do any work. fm-spawn now prefixes the claude launch with firstmate's own resolved CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config store firstmate is authenticated with. An unset value is the single-store default and adds no prefix; non-claude harnesses are unaffected. Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set, omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test helper so launch assertions no longer depend on the developer's environment. * no-mistakes: apply CI fixes
…guid#1233) * fix: preserve dispatch harness identity * no-mistakes(review): Fix Grok counterfactual tuple validation * no-mistakes(document): Scope dispatch authentication to selected tuple * fix: restore dispatch instruction budget * no-mistakes(review): Scope dispatch authentication after candidate selection
Two conflicts, both resolved by keeping the Windows branch and taking upstream's work wholesale on the POSIX side. bin/fm-session-lock-lib.sh: upstream rewrote fm_harness_ancestry_pid to walk 16 hops and, for Claude, to keep walking a contiguous claude-named run to the OUTERMOST pid (kunchenguid#1206), because the Stop hook fires several levels below the lock owner. Windows short-circuits into its own walk before that loop, so the new POSIX body is taken unchanged and the Windows dispatch sits above it. tests/fm-backend.test.sh: upstream refined the old-bin fixture comment and list; kept upstream's comment and the union of the sibling lists, since this branch's five extra entries are required by the transitive-source-closure guard upstream does not have. Notable in what arrived: upstream added its own Windows/MSYS support for watcher-arm confirmation (kunchenguid#1212), in a different area from this branch's work (spawn, session lock, symlinks, line endings). It carries a second uname cache in bin/fm-wake-lib.sh alongside this branch's platform seam; left as-is rather than refactored, so a file upstream is actively changing does not become a permanent conflict point. Upstream's kunchenguid#1195 adds a CLAUDE_CONFIG_DIR env prefix to the launch line, which is one more reason the Windows pane must be a POSIX shell before the launch is sent. The auto-merge placed it correctly, ahead of the pane setup.
Upstream's kunchenguid#1206 fixed the POSIX ancestry walk: Claude Code's Stop hook fires as a bg-spare worker several levels below the session that actually holds the lock (hook shell -> claude bg-spare -> claude bg-pty-host -> claude -> claude), so a first-match-wins walk resolved to the worker. The auto-arm then treated its own session as an unrelated live owner and never armed the watcher. The Windows walk had the same shape and the same bug. Left alone, the two platforms would disagree about which pid owns a lock, and tokenless watcher re-arm would be silently dead on Windows for the project's primary harness. Both Windows stages now share one match rule with the POSIX walk: first match wins, except that a claude-named match extends through a contiguous run of claude-named ancestors to the outermost pid, bounded by the first non-match so an unrelated claude further up the real process tree is never absorbed. Hop limit raised from 8 to 16 to match, since the bg-spare chain is deeper than the original bound. The deferred node/python interpreter check moves to a second pass over the same chain, so it still costs nothing unless no direct name match exists anywhere. The rule is pinned as a pure function over synthetic chains, because the end-to-end test upstream added stages real nested processes and therefore only exercises the POSIX walk; Windows resolves ancestry from process-table snapshots a test cannot stage as live processes. Verified on Windows: fm-claude-stop-autoarm passes, including upstream's new bg-spare case. Windows lane 26/26, lint clean.
`git grep -F "/Users/"` finds nothing on Windows. Git Bash rewrites an argument that looks like an absolute POSIX path into a Windows one before the native git.exe sees it, so the pattern arrived as a path under the Git installation directory and matched nothing. The invariant therefore passed without checking anything on Windows, which is the worst shape of failure: a green check that inspected nothing. Measured on the same tree: 0 matches with the literal pattern, 1 with either MSYS2_ARG_CONV_EXCL='*' or a regex whose first character is a bracket expression. The bracket form is used because it needs no environment variable and behaves identically on every platform, and it also keeps the property the old string-splitting trick provided: the pattern cannot match its own source. Also fixes the violation the working check immediately caught, in the fixture chain added by the previous commit. Same class as the python3 Store-alias bug, and the second one found on Windows: a check that resolves, reports success, and validates nothing.
This was referenced Jul 31, 2026
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.
Three commits: a faithful merge, then two fixes the merge exposed.
1. The merge
11 upstream commits. Two conflicts.
bin/fm-session-lock-lib.sh- upstream rewrotefm_harness_ancestry_pid(kunchenguid#1206) to walk 16 hops and, for Claude, to keep walking a contiguous claude-named run to the outermost pid. Windows short-circuits into its own walk before that loop, so upstream's new POSIX body is taken unchanged with the Windows dispatch above it.tests/fm-backend.test.sh- upstream refined the old-bin fixture comment and list. Kept upstream's comment and the union of the sibling lists; this branch's five extra entries are required by the transitive-source-closure guard upstream does not have.Worth knowing about what arrived:
bin/fm-wake-lib.shalongside this branch's platform seam. Left as-is: refactoring it would make a file upstream is actively changing into a permanent conflict point.CLAUDE_CONFIG_DIR=prefix to the launch line, which is one more reason the Windows pane must be a POSIX shell before the launch is sent. The auto-merge placed it correctly, ahead of the pane setup.2. Windows ancestry walk had the same bug upstream just fixed
Upstream's kunchenguid#1206 describes a chain of
hook shell -> claude bg-spare -> claude bg-pty-host -> claude -> claude(lock). The Windows walk had the same shape and the same bug, and the nesting is confirmed present on Windows.Left alone, the two platforms would disagree about which pid owns a lock, and tokenless watcher re-arm would be silently dead on Windows for Claude Code - the primary harness here.
Both Windows stages now share one match rule with the POSIX walk: first match wins, except a claude-named match extends through a contiguous claude-named run to the outermost pid, bounded by the first non-match so an unrelated claude further up the process tree is never absorbed. Hop limit 8 to 16 to match.
The rule is pinned as a pure function over synthetic chains. Upstream's end-to-end test stages real nested processes, so it only exercises the POSIX walk; Windows resolves ancestry from process-table snapshots a test cannot stage as live processes.
3. A repo invariant was passing without checking anything on Windows
CI caught a violation in the fixture chain added by commit 2. Reproducing it locally revealed why it had to be CI that caught it:
git grep -F "/Users/"finds nothing on Windows, because Git Bash rewrites an argument that looks like an absolute POSIX path into a Windows one before the nativegit.exesees it. The pattern arrived as a path under the Git installation directory and matched nothing.Measured on the same tree: 0 matches with the literal pattern, 1 with either
MSYS2_ARG_CONV_EXCL='*'or a regex whose first character is a bracket expression. The bracket form is used: no environment variable, identical behaviour everywhere, and it keeps the property the old string-splitting trick provided - the pattern cannot match its own source.Same class as the python3 Store-alias bug, and the second one found on Windows: a check that resolves, reports success, and validates nothing.
Verification
CI 10/10 green. Windows lane 26/26 and lint clean locally.
fm-claude-stop-autoarmpasses on Windows including upstream's new bg-spare case - the test that would have caught commit 2's bug had the walk been shared from the start.