Repository navigation
feat(bin): merge upstream firstmate main into the fork - #6
Merged
Merged
Conversation
…d#4490) * fix(codex): preserve announced recovery across checkpoints * no-mistakes(review): Make announced-recovery checkpoint test reproduce the regression * no-mistakes(review): Separate 2026-10-07 checkpoint fixture verification from original run * no-mistakes(test): Defer TERM exit to clean up quiet checkpoint watcher locks * no-mistakes(document): Update checkpoint recovery and signal documentation * no-mistakes(ci): Updated both lock-acquire retry loops to honor the watcher’s TERM stop status, so held-marker cleanup exits while retaining stale lock evidence. Set the --stop test’s watcher poll to 1 second to fit its 5-second stop bound. Syntax checks and the full watch-arm suite passed; the targeted held-marker test passed during the triage suite, which timed out after 600 seconds before completing. Did not change or attempt to fix fm-supervision-host.test.sh * no-mistakes(ci): Removed the cleanup-bound test that incorrectly expected a TERM-stopped watcher to retry a contended lock acquisition. TERM cleanup intentionally stops after the failed attempt; the remaining held-lock test verifies it exits and retains stale lock evidence. The two relevant TERM tests passed, as did shell syntax and diff checks * Revert "no-mistakes(ci): Updated both lock-acquire retry loops to honor the watcher’s TERM stop status, so held-marker cleanup exits while retaining stale lock evidence. Set the --stop test’s watcher poll to 1 second to fit its 5-second stop bound. Syntax checks and the full watch-arm suite passed; the targeted held-marker test passed during the triage suite, which timed out after 600 seconds before completing. Did not change or attempt to fix fm-supervision-host.test.sh" This reverts commit 44ce29b. * Revert "no-mistakes(ci): Removed the cleanup-bound test that incorrectly expected a TERM-stopped watcher to retry a contended lock acquisition. TERM cleanup intentionally stops after the failed attempt; the remaining held-lock test verifies it exits and retains stale lock evidence. The two relevant TERM tests passed, as did shell syntax and diff checks" This reverts commit e1e3706. * Revert "no-mistakes(test): Defer TERM exit to clean up quiet checkpoint watcher locks" This reverts commit 9877bf9. * fix: let checkpoint watcher clean up before timeout * no-mistakes(review): Scope checkpoint timeout escalation and restore signal docs * no-mistakes(review): Fix Perl checkpoint timeout escalation * no-mistakes(document): Clarify checkpoint recovery documentation * no-mistakes(document): Document isolated checkpoint regression command * no-mistakes(ci): Tied direct-checkpoint SIGKILL escalation to the effective cleanup bound, preserving the five-second minimum and leaving the supervision-host path unchanged. The targeted checkpoint suite, shell syntax checks, ShellCheck, and diff checks pass. CI was not manually rerun --------- Co-authored-by: jopo <jopo@JP-M2mini.local>
…uid#4395) * feat(bin): add Forgejo (tea) as a third forge alongside GitHub and GitLab Extends the existing provider-tagged PR/MR dispatch in fm-pr-lib.sh, fm-pr-poll.sh, fm-pr-check.sh, and fm-pr-merge.sh with a forgejo arm, following the same pattern already established for GitLab. tea addresses a repository by owner/repo slug only and resolves the host through a named "tea login" rather than a URL, so the login is matched against the PR's host fresh at arm and merge time. tea's own merge subcommand exposes no head-binding flag, so the merge itself calls Forgejo/Gitea's REST API directly via "tea api" with head_commit_id, verified against a real disposable instance to behave like --match-head-commit/--sha; that same verification found that "tea api" reports an HTTP-level failure with exit status 0, so a merge is confirmed only by reading the pull request back afterward, never by trusting the command's own exit code. Adds forgejo as a value of the registry's existing forge= binding (the closed set becomes none|gerrit|forgejo, with its refusals unchanged), so a generated brief and its direct-PR Definition of done name tea before any PR exists and bin/fm-spawn.sh's forge agreement and bin/fm-promote.sh carry it like gerrit. Documents the new value in docs/configuration.md, docs/architecture.md, and a new maintainer-verification doc mirroring docs/gitlab-merge-watch.md. * no-mistakes(review): Redact private Forgejo host/IP from evidence doc * no-mistakes(review): Fix doc's tea-login JSON shape; dedupe host validators * no-mistakes(document): Add Forgejo to README doc index and scripts.md forge list * no-mistakes(lint): {"summary": "fix(bin): drop unused FORGEJO_LOGIN capture in fm-pr-check.sh"} * fix(bin): refuse --allow-red on Forgejo instead of dropping it silently The attended --allow-red waiver landed on main while the Forgejo arm was being written, so its applicability guard only ever classified GitHub and GitLab. On a Forgejo pull request the flag parsed, and then nothing read it: forgejo_verify_mergeable requires the head's combined commit status to be exactly "success", with no per-check name to match a waiver against. The merge still refused, so no red Forgejo merge was ever possible, but it refused without saying the waiver had been ignored. Extend the existing GitLab guard to cover Forgejo for the same reason GitLab has it: both collapse their checks into a single head verdict, so the green requirement is absolute and a named waiver cannot apply. * fix(bin): poll a Forgejo pull request by number through tea api The merge watch listed the 50 most recent pull requests and filtered to the watched index, so in a repository with more newer pull requests a merged watch never fired. Read the one pull request through tea api instead and wake only on a boolean merged field of true, since tea api exits 0 on HTTP errors. Share the tea login match as fm_pr_forgejo_login for every sourcing script and add fm_pr_forgejo_read_record for the state, merged flag, and head. Arming now also requires jq, which the poll needs to read the record. * fix(bin): gate a Forgejo merge on every status page and on draft state The combined-status endpoint computes its state from one page of contexts, so a failing context past the first page could leave the head reading green. Read every page until an empty one and require each to be success. Also refuse a pull request the API reports as a draft. * fix(bin): prove a Forgejo task landed from its merged pull request at teardown pr_is_merged read the recorded pr= only through gh, so a Forgejo task always fell back to the content check and refused landed work whenever that check was inconclusive, such as when later default-branch commits conflict with the merged tree. Read a recorded Forgejo URL through fm_pr_forgejo_read_record and apply the same containment check against its head. * feat(bin): read Forgejo pull requests in crew-state and review-diff fm-crew-state reports a terminal passed run's Forgejo pull request as open, closed, merged, or unknown through fm_pr_forgejo_read_record, and fm-review-diff resolves a /pulls/<n> URL to the refs/pull/<n>/head ref Forgejo serves. Document the by-number read, the paged status gate, and the draft gate, and mark what the evidence instance did not exercise. * fix(bin): parse tea logins as JSON and close the remaining Forgejo gaps The tea login match split each line of `tea login list --output json` on quotes and read field 4, which only works while tea pretty-prints one field per line. A login printed as one compact object put the login name where the url belonged, so arming, merging, and polling refused every Forgejo host. fm_pr_forgejo_login and the static poll's own copy now parse the list with jq, matching each login's url host with scheme, userinfo, port, and path stripped, and still refuse zero or several matches. --allow-missing is now refused on a Forgejo pull request the way --allow-red already is, instead of being parsed and ignored: the merge already requires the head's combined commit status to have succeeded. The forge=forgejo Definition of done read the draft state back with gh-axi pr view and cleared it with gh-axi pr ready although the PR is opened with tea; both publishing modes now read the draft state through tea api and clear Forgejo's work-in-progress title prefix with tea pulls edit. * no-mistakes(review): Retry Forgejo merge confirmation read before failing * no-mistakes(document): Document Forgejo draft gate and merge confirmation retry * fix(bin): merge Forgejo pull requests with the repository's default style When the caller named no merge method, a Forgejo merge always sent Do=merge, so a repository that allows only squash or rebase merges refused every default call. The style now comes from the repository's own default_merge_style, read live through the same tea login, and a merge is refused when that value cannot be read or is not a known style rather than replaced by one chosen here. A method the caller names still wins. --------- Co-authored-by: dna <dna@local>
* fix: serialize secondmate liveness relaunches across task set * no-mistakes(review): Remove relaunch wait override and test bounded contention * no-mistakes(document): Clarify secondmate relaunch locking and timeout documentation * chore: keep local Stele binding out of shared template * no-mistakes(document): Correct stale secondmate recovery timing comment * chore: exclude local Stele binding from relaunch fix * ci: retrigger CI after a timed-out test shard * no-mistakes(document): Correct secondmate recovery ordering documentation
…unchenguid#7172) * feat(bin): gate firstmate-upstream publication behind a declared home role Users who clone firstmate and run it as-is got a surprise: when their agent hit a firstmate bug, it could drive a pull request at upstream because nothing distinguished the maintainer's home from an ordinary user's. Every home now resolves to user/operator unless the gitignored config/firstmate-role grants a maintainer scope, or a single task carries the captain's recorded --upstream-consent on the brief, spawn, and promotion. fm-brief, fm-spawn, and fm-promote apply one fail-closed gate in bin/fm-role-lib.sh; the role is never inferred from gh identity, remotes, permissions, or CI, and secondmates honor only receipt-proven inherited grants so a child cannot grant itself. Session start reports the resolved role with no network call. Ordinary project delivery and the clone-and-run setup flow are unchanged. * no-mistakes(review): Harden firstmate role parsing, normalization, and inheritance * no-mistakes(review): Detect firstmate by lineage; drop consent fallback; fix claims * no-mistakes(document): Correct and consolidate Firstmate role documentation
…henguid#7248) * fix: narrow Pi before-agent prompt events * no-mistakes(review): Restore processing-run abort detection and type Pi test events
…guid#6608) * docs: let a home declare authoritative external memory tiers AGENTS.md section 6 routed durable knowledge only to firstmate-local files, so a home whose captain keeps reasoning and facts in external stores had no tracked rule keeping those stores authoritative over the per-machine data/ cache. Add a generic home-declared Memory layering contract (schema in docs/configuration.md), make firstmate the capture path for workers whose harness cannot reach a declared tier, and teach the stow skill to route to declared tiers first and never prune a tier-owned entry without capturing it. * no-mistakes(review): Report missing memory-layering declaration and index tier docs * no-mistakes(document): Narrow memory-tier re-seed step to captain re-declaration * no-mistakes(ci): Environment: 2026-10-05, host wildrobot, Linux 7.0.0-34. Finding ci-1 (Greptile): a tier-owned fact could be lost when its declared tier was unreachable from the session. - The invariant that was broken: until the declared tier holds a tier-owned fact, that fact must stay in a memory file loaded at startup. - Where it could be broken: the declared-tier guard, which said to archive the entry to the never-loaded cold tier; the routing step for new findings, which only mentioned the receipt; and the budget-eviction and offload eligibility rules. Fix, made only in .agents/skills/stow/SKILL.md: - **New marker.** Added `<!--t:NAME-->` to the existing compact marker list, so its meaning lives in one place. It marks a tier-owned entry that tier NAME does not hold yet. Stow never prunes, ages out, evicts, offloads, consolidates away, or archives an entry carrying it, whatever its tier clock says. A later pass that can reach NAME captures the entry there and removes the marker. - **Declared-tier guard.** "still archive rather than delete" now says to leave the entry in place with its pending-capture marker, and the receipt still names it. - **Routing step 2.** When the tier is unreachable, a new finding is written to the startup-loaded local file its class would otherwise use, or kept where it already lives, with the marker. It is still reported as an exception in the receipt. - **Budget eviction and offload eligibility.** Both now exclude entries pending capture, so the convergence pool is counted correctly. - **Receipt bullet.** Changed "finding or pruned entry" to entries left carrying the pending-capture marker, because those entries are no longer pruned. docs/configuration.md ("protects tier-owned entries from pruning") and AGENTS.md section 6 are still accurate, so I left them unchanged. No new file, queue, script, or digest section. One sentence per line, plain dashes. Verification: tests/fm-stow-cascade.test.sh, tests/fm-documentation-audiences.test.sh, and tests/fm-lint.test.sh all pass * docs: keep undeclared memory-tier path silent and close stuck tier markers Drop the stow receipt line reporting a missing Memory layering declaration (AGENTS.md section 6, the stow skill, docs/configuration.md) so a home with no declaration behaves exactly as before. Name pending-capture entries in the exempt floor wording, and say how stow retargets or clears a pending-capture marker whose tier was renamed or retired.
* fix: keep unsupported contributions unmeasured during polling * no-mistakes(document): Clarify unsupported contribution polling documentation * no-mistakes(review): Match literal GitHub hostname and cover unsupported lookalikes * no-mistakes(review): Separate durable ownership compatibility from GitHub observation eligibility * no-mistakes(document): Clarify unsupported contribution coverage and ownership compatibility
…er-strip hooks path (kunchenguid#6341) * fix(bin): keep inherited Git credential helpers when installing the trailer hooks path Problem: Every fleet launch prefixed the worker command with `export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=...`. That replaced any `GIT_CONFIG_*` entries the pane already carried, so a scoped HTTPS credential setup such as `credential.helper=` followed by `credential.helper=!gh auth git-credential` was silently dropped. Git then fell back to the system or global helper (for example `osxkeychain` from the system gitconfig on macOS), so a worker meant to use a scoped token pushed with the user's personal credentials. Reproduction on main: GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=credential.helper GIT_CONFIG_VALUE_0= GIT_CONFIG_KEY_1=credential.helper GIT_CONFIG_VALUE_1='!gh auth git-credential' sh -c "<emitted launch>; git config --get-all credential.helper" prints none of the inherited entries, only the system helper (`osxkeychain` from Homebrew's system gitconfig on the reporting machine). Fix: The launch prefix now appends `core.hooksPath` at index `GIT_CONFIG_COUNT` and increments the count, resolved in the destination shell after any `config/launch-env-allowlist` filtering. The inherited count is validated as decimal before shell arithmetic, leading zeros are normalized (Git accepts `02`), and a count that cannot be extended within Git's signed int refuses. A refusal uses `return 1 2>/dev/null || exit 1`, so when the pane sources the launch file the error stays visible and the interactive shell survives instead of the pane dying with the message. Tests: `tests/fm-spawn-dispatch-profile.test.sh` gains a behavior test that runs the real emitted launch under sh, bash, and zsh with inherited credential-helper entries and counts `2` and `02`, then asks Git for `credential.helper` and `core.hooksPath`. The same test checks that an invalid or overflowing count refuses, and that sourcing the launch file from an interactive shell keeps that shell alive with the error visible. The exact-launch assertions in the dispatch-profile and Kimi suites now extract the emitted prefix through one `fm_test_launch_git_prefix` helper in `tests/lib.sh` instead of two hand-copied strings, whose content the behavior test covers. Risk: Top-level `return` outside a sourced file is unspecified by POSIX; verified on macOS sh (bash 3.2 in POSIX mode), bash, and zsh, and every path still ends nonzero before the worker command runs. The hook wrapper still unsets only `GIT_CONFIG_COUNT` and index 0, which is sufficient because Git ignores indexed entries once the count is unset. A launch that forwards `GIT_CONFIG_COUNT` without all of its indexed keys now fails in Git instead of being masked by the overwrite. * no-mistakes(review): Keep inherited Git config in hooks and fish-safe launch * no-mistakes(document): docs: describe appended hooksPath keeping inherited Git config * test: run the fish pane launch cases in CI Install fish in the portable serial lane and run the credential-append and invalid-count refusal cases under fish. Under CI the spawn test fails instead of falling back to the static syntax check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: strip the Git config launch prefix before reading the Claude doorbell The trust suite evaluated the whole launch command to find the final prompt argument, so it now also sourced the per-spawn git-config env file that the fake pane never writes. Drop that prefix the same way the dispatch suite does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: share the launch command parser across the spawn suites The trust suite failed in CI because its copy of the launch parser missed the Git configuration prefix the dispatch suite's copies already stripped. Move the prefix and export stripping into tests/lib.sh next to fm_test_launch_git_prefix so the three parsers cannot drift again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(bin): canonicalize env-file hooks path and tighten launch-config tests env-file now writes the canonical (pwd -P) spelling of the hooks directory, matching what install records, so a symlinked path no longer makes the hook wrapper miss its own entry and silently skip the repository's hook. A new strip test passes a symlinked hooks path through env-file and fails without this change. The launch-env-allowlist test now also asserts the appended core.hooksPath, so a regression that drops the strip-hook entry only on the env -i path is caught. Without fish installed locally, the launch test now prints an explicit fish skip line and limits its pass message to sh, bash and zsh, instead of claiming every pane shell after only a static token check. * no-mistakes(review): Drop CI fish install and CI-only fish refusal * no-mistakes(document): Document GIT_CONFIG_COUNT overflow refusal in configuration docs * test: prove a refused Git config count never starts the worker The invalid-count refusal checks only asserted a nonzero status and the helper's own error text. If the launch line kept going after the refusal, the worker probe's final `git config` also failed under the bad count, so the checks still passed. The probe now prints a marker on stderr, and every refusal case in sh, bash, zsh and fish asserts the marker never appears. Dropping the `|| return 1 2>/dev/null || exit 1` guard from the launch line now fails the test. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…available (kunchenguid#6810) * fix(bin): name the failing read when a contribution observation is unavailable An unavailable contribution observation recorded one constant error and discarded the failing read's exit status and error output on cleanup, so a network failure, a forge refusal and a structural check failure all looked the same and left nothing to diagnose once the next good read cleared it. The record's error and the unavailable line now name the first failing step: the read's label, exit status, budget seconds left and the first line of its error output, or the structural check that refused the observation. Budget refusal stays silent and unmeasured as before. * no-mistakes(review): Label closing head read's structural failure as its own cause * no-mistakes(document): Note structural refusals record contribution errors too
…unchenguid#6365) * fix(spawn): give Orca workers an opt-in scoped environment source Fixes kunchenguid#6339 On the Orca backend, `config/launch-env-allowlist` expands each allowlisted name in the destination pane, and that pane is an Orca terminal whose environment comes from the Orca app and the user's shell startup files. Allowlisted credentials therefore resolve to the user's personal values or to nothing, with no warning, while the same allowlist scopes tmux workers correctly. Add an opt-in `config/launch-env-tmux-session` naming one tmux session. On a fresh Orca spawn, before the worktree or terminal is allocated, fm-spawn reads each allowlisted name from that session's local environment with `tmux -u show-environment -t =<session> <name>`. The worker launches with the existing `env -i` boundary and operational floor, the snapshotted values in place of destination expansion, and `GIT_CONFIG_NOSYSTEM=1` so a system credential helper cannot answer for the worker. The source requires an allowlist, so tmux's automatic update-environment names such as `SSH_AUTH_SOCK` are never forwarded unless listed. A listed name missing from the session, an unreadable session, or a malformed selector refuses before allocation; an explicit `-NAME` record stays a deliberate unset. Values are shell-quoted only in the existing owner-only staged launch file, never terminal input, and the pane deletes that file as sourcing starts; a deletion failure refuses the worker launch and keeps the interactive shell. `-u` is required because tmux's non-UTF-8 client output replaces newlines and non-ASCII bytes in values with underscores. The source is opt-in and nothing changes for homes that do not create the file. When `config/backend` is orca, the allowlist is nonempty, and no source is configured, fm-spawn now prints a stderr warning that allowlisted names resolve from the Orca terminal, so the mismatch is no longer silent. It warns rather than refuses because refusing would stop every existing Orca home that uses an allowlist, which the triage asked not to assume. Other backends, relaunch (already refused on Orca), and secondmate inheritance are unchanged; the selector is not in the inherited configuration list. The scoped Git credential helper case also needs kunchenguid#6341, which stops fm-spawn's `core.hooksPath` entry from overwriting inherited `GIT_CONFIG_*` entries; this change does not depend on it. Tests in `tests/fm-spawn-orca-worktree.test.sh`: - A real-tmux regression on a private socket checks exact multiline UTF-8 values under absent and C locales through sh, Bash, and zsh, refusal of names that exist only server-globally or only inside another value, and exclusion of the automatic `SSH_AUTH_SOCK`. - Fake-Orca spawns execute the real staged launch file under synthetic personal credentials and cover the configured source, an empty allowlist, refusal before allocation for each invalid setting, file privacy and deletion before the worker runs, and the deletion-failure path. - The warning appears for a nonempty allowlist without a source and not for an empty allowlist or a configured source. Risk: the source keeps tmux and the named session as dependencies of Orca spawns, and secret values sit in a 0600 file under a 0700 directory until the pane sources it. It is not a filesystem sandbox; a worker can still read the user's credential files unless the harness restricts that. The hardened path was verified with the regressions above, not with a live Orca spawn. * no-mistakes(review): Harden Orca launch-file cleanup, floor unsets, and stat portability * no-mistakes(review): Scope Orca abort cleanup to the staged launch file * docs: scope Orca source to fresh spawns and qualify pane values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: state that Orca relaunch is refused Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * no-mistakes: apply CI fixes * no-mistakes(review): Keep Orca launch file while delivered terminal stays open * no-mistakes(document): Qualify fm-spawn help's destination-pane expansion claim for Orca * fix(spawn): keep launch-send failures fatal and compare tmux bytes per version The Orca launch-file cleanup wrapped both launch sends in a condition, which runs outside errexit. A failed send on tmux and every other non-Orca backend then continued as success, so a post-publication relaunch failure no longer failed closed. Each send failure now exits again, and Orca source mode removes its staged file first. The real-tmux regression asserted that invalid UTF-8 bytes survive tmux itself, which holds on tmux 3.6a but not on the CI runner's tmux. It now asserts that the snapshot reproduces exactly the bytes the installed tmux reports for that name. * fix(spawn): reclaim an Orca launch file the pane never runs In Orca source mode the pane unlinks the credential-bearing launch file as it starts sourcing it. A pane that accepted the source command but never ran it left the file on disk until teardown, because the abort cleanup keeps the file for a delivered, still-open terminal. After delivery, fm-spawn now waits up to 30 seconds for the pane to consume the file; if it has not, fm-spawn removes it and fails the spawn with a status line naming the unrun command. The snapshot already reads and writes tmux output as latin1, so bytes that are not valid UTF-8 reach the launch file unchanged. A deterministic fake-tmux regression now pins that, independent of how the installed tmux stores such values. The live-terminal test now drives a slow pane and fails unless the spawn waits for it, and the controlled-source test reads the file the fake pane consumed. * no-mistakes(review): Keep Orca terminal open when launch file is unconsumed * no-mistakes(review): Refuse Orca worker when launch file already reclaimed * no-mistakes(review): Make Orca spawn's deadline launch-file reclaim exclusive * no-mistakes(document): Document Orca launch-file reclaim keeping the terminal open * fix(spawn): reclaim an interrupted Orca launch file and keep errexit panes open An HUP, INT or TERM after the source command reached the Orca terminal left the credential-bearing launch file on disk: bash ran the EXIT trap with status 0 and abort cleanup kept a delivered file for the pane. Source mode now traps those signals until the pane consumes the file, and abort cleanup reclaims it with the same non-forced unlink arbitration as the consume deadline, refusing the worker when the spawn wins and leaving the terminal open. The delivered source line now keeps a pane shell with errexit set open when the launch is refused, including when the file was already reclaimed. * no-mistakes(review): Trap signals as soon as Orca launch file is published * no-mistakes(document): Record pre-send interrupt coverage in Orca verification notes --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…re, including absolute paths (kunchenguid#7307) * fix(composer): treat OpenCode >= 1.18 right-aligned cwd:branch as furniture OpenCode 1.18.31 draws the worker pane's working directory and branch right-aligned inside the left-bar composer rows above the Build footer, wrapped over up to three rail-width fragments whose final fragment continues onto the footer row's right end. The fragments are RGB 128 grey with luminance exactly on the ghost-luma ceiling, so they survive ghost stripping, and the left-bar classifier read them as unsent typed text: every idle 1.18.31 pane answered pending, which skipped the steering doorbell, the watcher's re-ring ladder, and /exit typing. _fm_composer_leftbar_cwd_start recognises the run structurally and position-gated: contiguous single-token rows immediately above the footer row whose gap after the bar exceeds half the row, whose fragments plus the footer row's gap-separated right-hand tail concatenate to a path, optionally followed by :<branch>. A path-shaped string typed at the composer's left edge is still typed text, so a wrong call can only defer, never inject. Extraction shares the same rule so extracted composer content stays user-scoped. * no-mistakes(review): drop dead footer-tail concat, add live short-cwd fixture * no-mistakes(review): accept only ~/ cwd rail; lone slash stays pending * no-mistakes(review): require footer row for leftbar cwd furniture walk * no-mistakes(review): Require topmost cwd fragment to carry ~/ prefix * no-mistakes(document): Correct cwd furniture recognition doc to topmost-fragment check * fix(composer): gate cwd rail on region width; correct version story Review round on the independent PR review: the position gate now measures the gap against the region's width (the widest trimmed plain row across first..last), so a short space-prefixed home path typed into an otherwise empty composer no longer reads empty; the right-half lone-token residual is pinned explicitly. The cwd:branch furniture is documented as present since OpenCode 1.18.0 (upstream anomalyco/opencode#36457, merged 2026-07-11), not a 1.18.31 layout change. The 1.18.30 capture comment notes it omits the directory footer, and a cropped-composer extraction assertion scopes user content to the typed row above the rail. * no-mistakes(document): correct OpenCode cwd-furniture version framing and doc typos * fix(composer): accept an absolute cwd rail outside $HOME as OpenCode furniture Refs kunchenguid#4632, builds on kunchenguid#5030. * no-mistakes(review): Harden live abs-cwd check waits and qualify rail comment * no-mistakes(review): Fix composer-lib relative path in OpenCode harness doc * no-mistakes(test): Relax live check's DONE wait for wide OpenCode panes * no-mistakes(document): Separate absolute-cwd verification output block from following prose --------- Co-authored-by: Konrad Heimel <konrad.heimel@corp.ovh.com>
…mpt (kunchenguid#6682) * fix: name the Claude exit picker on existing stale wakes A pane parked on that picker still read as healthy quiet after Enter was refused. The existing stale and wedge reports now carry the dialog name, including through away-mode escalation. Refs kunchenguid#4754 * no-mistakes(review): fix: report exit picker on terminal-status away stales * no-mistakes(document): docs: describe exit-picker reporting in stale supervision * no-mistakes(review): fix: drop away stale marker when second-mate picker closes * fix: keep the exit-picker name off paths main does not already run A second mate stays exempt from stale capture, and a finished worker in away mode no longer ages into a wedge. The name remains on the stale and wedge wakes a worker already raises. * no-mistakes(review): name the exit picker on wedge deferral reasons * no-mistakes(review): limit exit-picker name to plain stale and wedge escalation
…uid#7308) * feat(afk): Spend-Obergrenze beim Eintritt aus der lebenden Worker-Zahl Ohne ausdrückliches --spend nimmt der Eintritt in den Away-Modus die Zahl der gerade im Home stehenden gewöhnlichen Worker als Obergrenze, damit der Modus laufende Parallelität nicht einengt. Der bisherige Default bleibt Untergrenze, 0 bleibt ausgeschlossen, und ein ausdrückliches --spend gewinnt weiterhin. Die Zählung bekommt mit `live-workers` genau eine Implementierung in bin/fm-afk-contract.sh. bin/fm-spawn.sh liest sie dort statt mit einer eigenen Schleife, sodass Eintritt und spätere Sperre nicht auseinanderlaufen können. * no-mistakes(document): Remove misplaced afk spend-cap plan --------- Co-authored-by: ladwein <ladwein@firstmate.bost8.thelad.loc>
…ion paths (kunchenguid#6965) * fix(bin): guide supervision through completed-scout cleanup * no-mistakes(document): Document completed-scout cleanup ownership and regression coverage * no-mistakes(test): Allow confined lab overrides and verify live scout cleanup * no-mistakes(test): Clarify per-task cleanup reporting and verify live outcomes
…o the job's timeout (kunchenguid#7240) * fix: recheck a worker paused on its own background job A worker that declares `paused:` on its own background job and then misses the job's completion stays idle until the long declared-wait recheck, because the first-sight alert spends the declaration's throttle and nothing else names when the job must have ended. The worker pause instructions now say that `until` for such a job is its start plus its timeout, and the watcher already rechecks once when that time passes. The recovery playbook now tells the supervisor to verify the condition a declared wait names before reporting the wait as healthy. Fixes kunchenguid#7239 * test: give the background-job until test headroom over its quiet round The first absorb round of the until test may take four poll cycles of up to 30 s each on a loaded runner. With `until` only 15 s ahead, that time could pass during the round and fail the test although the watcher behaves correctly. Set `until` 120 s ahead so the window covers those four cycles, as CONTRIBUTING asks of fixture windows. * chore: retrigger CI * chore: retrigger CI * no-mistakes(review): Rebuild bg-job until test with passed until, no sleep * no-mistakes(review): Restore bg-job absorb checks and assert due recheck marker --------- Co-authored-by: pedromuller-del <254459182+pedromuller-del@users.noreply.github.com>
…dapter (kunchenguid#6679) * feat: add Command Code crewmate and scout adapter Add `commandcode` as a verified worker runtime for crewmate and scout work only, so Firstmate can dispatch workers on Command Code models such as deepseek/deepseek-v4.1-flash. - Detection: the anchored `command-code` process title identifies the adapter in fm-harness.sh and the shared liveness classifier. - Launch: fm-spawn passes --yolo, --trust, --skip-onboarding, the model, an effort only where the model's accepted set is verified, and the typed launch brief after --; taste-learning state is kept out of git. - Busy state: a tracked mod, bin/fm-commandcode-mod.ts, is loaded per process with --mod and --mod-option, writing run_start/run_end/session end through fm-busy-event.sh; no user or project settings are edited (Command Code's --config persists globally, so it is never used). - Control: one Escape interrupts; the idle double-Escape Rewind picker is a recognized hazard that exit refuses and interrupt dismisses; /quit exits. - Composer: the bright truecolor placeholder is read with an identity-scoped ghost ceiling, and the tmux adapter supplies the identity for a cursor parked below the footer; Herdr reports it natively as `cmd`. - Attribution: the fleet commit-msg strip removes the CommandCodeBot trailer. - Docs, harness reference, verification record, portable regressions, and the FM_COMMANDCODE_SIGNALS_LIVE live guard. * no-mistakes(review): Drop flash-fast effort pairs; correct identity-probe cost comments * no-mistakes(ci): I fixed both Greptile findings the way you asked. Both new regression tests fail on the old code and pass now. The changes are not committed. **ci-1: Command Code on zellij, cmux and orca.** The rule that must hold is that a Command Code worker only runs on a backend that tells Firstmate the pane is Command Code. Without that, its idle placeholder never reads as an empty composer, so steering stalls. `bin/fm-spawn.sh` now refuses `--harness commandcode` unless the backend is tmux or herdr. The error reads: `commandcode is verified on the tmux and herdr backends only; backend=<b> cannot prove its composer empty. Select backend tmux or herdr.` - The check runs after both the harness and the backend are known, so it covers fresh spawns and `--relaunch`. - It runs before any pane, worktree, task record or busy state is created. - New test in `tests/fm-commandcode-harness.test.sh`: tries a scout spawn on each of zellij, cmux and orca (with a fake `orca` that reports ready). It checks for the refusal and that nothing was launched, no task record was written and busy state was not armed. It needs no real backend installed. - Documented in `docs/configuration.md`, the Command Code adapter reference and the `fm-spawn.sh` header. **ci-2: an old process signalling its replacement.** The rule is that the `turn-ended` notification may only be raised under the same generation check as the idle write. The mod used to touch the file itself after the writer returned, which left a gap for a relaunch to slip in. - `bin/fm-busy-event.sh apply` takes a new `--turn-ended` flag. It touches `state/<id>.turn-ended` while still holding the writer lock, after the generation check passes. A stale generation is refused with no touch, and the flag is rejected on any subcommand other than `apply`. - `bin/fm-commandcode-mod.ts` no longer touches files itself. On `run_end` it passes `--turn-ended` unless the turn was interrupted. - New test in `tests/fm-commandcode-harness.test.sh`: a fake writer accepts the idle write and then arms a replacement before the mod continues. It asserts no notification appears. - New test in `tests/fm-busy-state.test.sh`: the notification is raised for the current generation, not for a stale one, and not outside `apply`. **Checks run:** - Passing: `fm-commandcode-harness`, `fm-busy-state`, `fm-busy-adapter-wiring`, `fm-backend`, `fm-documentation-audiences`, and `bash -n` on the changed shell files. - `fm-bootstrap.test.sh` fails one case ("backend=herdr must fail closed on its own missing session CLI"). It fails the same way on an untouched copy of the branch, because herdr is installed at `/usr/bin/herdr` on this machine. - `bin/fm-lint.sh` could not run because ShellCheck 0.11.0 is not on PATH, so the changed shell files have not been linted. **Not changed:** Devin's hook does the same thing outside this PR: it runs `apply ... && touch` in `bin/fm-devin-config.sh`. That predates this PR and is not in the changed area, so I left it alone * no-mistakes(ci): I fixed ci-sc2016. ShellCheck is not installed on this machine, so I could not confirm that the lint warning is gone. **The rule that was broken:** the repo lints with ShellCheck at its default severity, which also fails on info-level notes. Any single-quoted `$` that is meant to stay literal needs a scoped disable comment that says why. Only one place in the changed area needed this: the racing-writer `printf` in `tests/fm-commandcode-harness.test.sh`. Its format string writes `"$2" "$3"` on purpose, so the generated fake writer reads its own arguments when it runs. **The fix:** I added one line just above that `printf`: `# shellcheck disable=SC2016 # positional params expand in the generated writer.` The repo already does this the same way, for example in `tests/fm-afk-launch.test.sh` and `tests/fm-ask-user-authority.test.sh`. Nothing else changed, so the test behaves exactly as before. **Checks run:** - `bash -n tests/fm-commandcode-harness.test.sh` passes. - `bash tests/fm-commandcode-harness.test.sh` passes all six cases, including the busy-mod case with the racing writer. **Not run:** `bin/fm-lint.sh` stops with "ShellCheck not found" because ShellCheck 0.11.0 is not on PATH and there is no copy in the repo. I did not install it, since installs outside the worktree are off limits here. The upstream lint run will be the first ShellCheck pass on this change. The change is not committed * no-mistakes(review): Hard-code Command Code ghost luma ceiling at 160 * no-mistakes(review): Test commandcode relaunch refusal off tmux/herdr before stop * no-mistakes(document): Note Command Code fixed ghost luma ceiling in config docs
Claude spills a hook output over 10,000 characters and shows only the first 2,000 plus a saved-file path. The re-emit prints captain.md and learnings.md last, so they never reached the session. Print a short cue first that names the saved file and the memory files, and document the cap. Refs kunchenguid#6303
…kunchenguid#7302) * fix: retry transient contribution reads once * fix: classify contribution read errors safely Address review findings for kunchenguid#7262 by parsing only gh status forms, recognizing real DNS errors, refusing GraphQL not-found retries, clearing per-URL diagnostics, and restoring exit-137 handling. * fix: recognize bare gh HTTP errors Complete kunchenguid#7262 review fixes by accepting bodyless gh status lines and keeping all GraphQL errors out of transient network classification. * no-mistakes(review): Classify gh network errors by Go error tokens * no-mistakes(document): Note why contribution 404 reads are never retried * no-mistakes(review): Retry message-less GraphQL HTTP 5xx on closing read * fix: retry transient contribution reads once * fix: classify contribution read errors safely Address review findings for kunchenguid#7262 by parsing only gh status forms, recognizing real DNS errors, refusing GraphQL not-found retries, clearing per-URL diagnostics, and restoring exit-137 handling. * fix: recognize bare gh HTTP errors Complete kunchenguid#7262 review fixes by accepting bodyless gh status lines and keeping all GraphQL errors out of transient network classification. * no-mistakes(review): Classify gh network errors by Go error tokens * no-mistakes(document): Note why contribution 404 reads are never retried * no-mistakes(review): Retry message-less GraphQL HTTP 5xx on closing read
…tion (kunchenguid#7338) * fix(bin): let a backlog captain hold bound the watcher's wedge escalation A stuck worker whose task was held for the captain through bin/fm-captain-hold.sh kept wedge-escalating every STALE_ESCALATE_SECS, because wedge_wait_evidence only read the status line. It now also reads the open backlog captain call (task_captain_call_open) and defers to the existing captain-facing long recheck, silent while the away record exists. * no-mistakes(review): Publish no wait age for backlog captain holds * no-mistakes(review): Let an elapsed paused-until line fall through to backlog hold * no-mistakes(document): Document backlog captain hold in wedge wait-evidence contract
…guid#5404) * fix(bin): parse Lavish list-form results and refuse unparsed blocks Lavish emits its queued prompts in TOON list form instead of table form as soon as the items stop being uniform, for example when some annotations carry a nested target object or a message carries an attachments table. The adapter only recognized the table header, so read reported a complete, empty result and answers closed nothing. Move the result parsing into one shared reader used by read, answers, and reconciles that accepts both forms, keeps only column-zero headers, skips nested structures only beneath the field that opened them, and counts anything it cannot account for as malformed. An unrecognized header naming the block is now malformed rather than an empty result, answers and reconciles exit nonzero when the result is not fully accounted for, and the queued-content check treats a list header as content. Refs kunchenguid#4726 * no-mistakes(document): Document Lavish capture formats and incomplete-result handling * test: expect captain_message_count for the open-board list-form capture
…uid#5835) * fix(pi): let the supervision branch claim Orca stale wakes The watcher keys Orca endpoints by terminal=, but the branch dispatcher only indexed window=, so every Orca stale row vetoed the wake scan and went to main. Index both endpoint fields. Local hotfix ahead of upstream; refs kunchenguid#5623. * test(pi): cover terminal-keyed Orca stale rows in branch dispatch Pins the terminal= endpoint lookup: a terminal-keyed stale row queued with another eligible row must resolve to its task in both routine and heartbeat scans, instead of vetoing the wake to main. * no-mistakes(document): Update supervision dispatch doc for Orca terminal-keyed stale rows * fix(pi): reject conflicting wake endpoint ownership Index task keys and window/terminal aliases through one ownership check. Ambiguous records must leave the wake on MAIN instead of selecting the last task and possibly bypassing another owner's open decision. Cover duplicate terminals and collisions with window and task keys in both record orders while preserving unique terminal routing. * no-mistakes(document): Document conflicting alias ownership as scope corruption * fix(pi): retain ownership claims from incomplete task records Check endpoint and task alias ownership before project eligibility so a projectless record cannot lose its open decision to another terminal owner. Require project context before decision filtering can grant routing scope. Cover missing and empty project fields in both record orders, and preserve refusal for a uniquely mapped terminal without project context. * no-mistakes(review): Fix canonical routing ownership and custom SDK event handling * no-mistakes(review): Preserve dedicated custom processing run ownership * no-mistakes(document): Clarify Pi event handling and fail-closed routing documentation
…anches (kunchenguid#7368) * feat(bin): add a forge=external review rail that never pushes task branches A project whose review tool uploads its own snapshot of the worker's local commits can now be registered [direct-PR forge=external]. Workers on such a project never need to push a branch to publish, and must never push a task branch to the shared origin. - fm-project-mode.sh: external joins the closed forge set; never detected, refused on local-only, and +yolo reports yolo=off with a refusal. - fm-dod-lib.sh: external ships direct-PR only; its first rule forbids origin and task-prefixed pushes, its Definition of done renders the home's config/external-review.md adapter verbatim, and the ready-report gate accepts `done: review <url> uploaded for review` on the URL alone or, with a recorded backup remote, once HEAD is on the backup namespace. - fm-git-push-guard.sh (new): per-task pre-push guard refusing pushes to origin or a URL matching origin's, task-branch and task-prefixed refs on any remote, and refs outside the backup prefix on the backup remote. - fm-git-strip-ai-trailers.sh: install gains --push-guard and --no-strip. - fm-spawn.sh: refuses yolo, a missing adapter, and an unusable backup remote before launch; installs the guard even under keep-ai-trailers; and records forge=external, backup_remote=, and backup_prefix=. - fm-review-check.sh (new): records review= and review_head= with no merge watch, and publishes the child's ready line in a secondmate home. - fm-promote.sh refuses promotion onto forge=external, because a scout launches without the guard. - Docs: docs/external-review-forge.md (design), configuration.md's new "External review tool" section, scripts.md, and the ship-landing and project-management skills. * no-mistakes(review): Drop string-grep hook assertion from external-review spawn test * no-mistakes(document): Note forge=external contract token and direct-PR-only composition
…uid#7387) Ein Lauf, den sendCustomMessage startet, traegt eine Custom-Nachricht und keinen prompt. Das Lesen des prompt an dieser Stelle warf, womit jeder solche Lauf mit einem Fehler auftauchte statt zu starten. Die Verengung dagegen steht seit 8c9c67d im Zweig, ein Test, der sie benennt, fehlte: die einzige Abdeckung lag beilaeufig in einem Abbruch-Fall der Abwesenheits-Haltung und verschwindet mit ihm. Der neue Fall erweitert den bestehenden Mirror-Test und prueft, dass ein solcher Lauf weder wirft noch als Dialog gespiegelt wird. Gegenprobe: ohne die Verengung wirft genau dieser Fall mit "Cannot read properties of undefined (reading 'trim')". Co-authored-by: thelad-dev <ladwein@firstmate.bost8.thelad.loc>
…nchenguid#7409) * fix(tmux): verify exact endpoint membership * fix(tmux): preserve exact target forms * test(tmux): align endpoint fixture lifecycles * test(tmux): guard isolated cleanup and shell reader * no-mistakes(review): Limit tmux .pane target split to window indexes
… plain row (kunchenguid#7395) * fix(bin): stop misreading opencode's own footer as pending composer text An idle OpenCode composer classified as `pending`, and that verdict gates every typed-plane action firstmate owns: fm-send skips the doorbell, and fm-control refuses both relaunch and exit. A firstmate operator lost a full session to it and had to type into worker panes by hand three times. Root cause: opencode 1.19 draws the mode/model footer's identifying cells de-emphasised (mode word RGB 67;133;190, middle-dot separators 111;110;105, luminances ~119.8 and ~109.7, both under fm_composer_strip_ghost's 128 threshold). That helper exists to answer "what did the human type?", so it deleted exactly the cells that identify the footer. The `^(Build|Plan)` anchor then became unreachable on every styled backend - tmux, Herdr, and Zellij - not just one pane, because the unstyled row keeps its styling bytes everywhere. The fix proves the footer from both forms of that row: the ghost-stripped content and the ANSI-stripped plain row. Proving it on the form ghost stripping cannot reach is the whole repair. What makes that safe, and why the previous argument was not: the earlier draft argued safety by noting that on a plain capture the two forms collapse to one string, so the verdict is unchanged. That is true and carries no weight, because styled=0 is the only configuration where the new proof does nothing. The exemption is safe because a row reaches the plain form only when the ghost-stripped row still fails the pattern, which means the stripped content is non-empty while its own leading Build/Plan cell was deleted as ghost. Nothing opencode draws de-emphasises a typed row's leading cells - only its furniture is drawn dimmed - so the only rows the plain form admits beyond the stripped one are de-emphasised furniture, and the mode footer is the only furniture opencode draws that opens with a mode word. That is self-confirming rather than asserted, and it is the argument to recheck when a new opencode release moves a colour. A review also caught a second change riding along in the same diff: the footer pattern had been widened from `[[:space:]]+` to `[[:space:]]*`, admitting `Build ·x` and a bare `Build ·`. Both read as more typeable than the footer, so they moved the verdict in exactly the direction this rule exists to prevent. No recorded opencode footer ends at the dot, so the original `+` matches every real capture and the widening is reverted. Verification: - fm-lint.sh exit 0 on pinned shellcheck 0.11.0 and actionlint 1.7.12. - fm-doc-audience-check.sh ok across 119 surfaces and 757 local links. - tests/fm-composer-lib.test.sh green via bin/fm-test-run.sh. - The new regression case fails on exactly one assertion when this change is reverted: "opencode 1.19.0 de-emphasised footer row: expected empty, got 'pending'", which is the reported symptom. - Confirmed end to end against live panes rather than only fixtures: a real idle opencode 1.19.0 worker classified empty after the change and pending before, and firstmate reached the worker over the steer channel afterwards. * no-mistakes(review): fix(bin): apply leftbar footer rule in content extractor; correct cursorless claim * no-mistakes(document): document opencode 1.19 footer furniture and extractor coverage * no-mistakes(document): document ghost-luma knob coupling to opencode footer furniture * no-mistakes(document): document opencode de-emphasised footer recognition invariant * no-mistakes(review): fix(docs,bin): correct ghost-luma knob direction, gate plain-row extraction * no-mistakes(review): fix(docs,bin,tests): correct ghost-luma knob wording, share plain-content helper, cover Herdr/Zellij idle * no-mistakes(document): narrow overstated backend coverage claim in runtime-backends.md * no-mistakes(document): document leftbar footer override and opencode 1.19 pointer
…7406) Der Kopf des Tests sagt, jedes Programm werde so gestartet, wie bin/fm-spawn.sh es startet. Im codex-Zweig von launch_cmd fehlte dabei --disable hooks, das fm-spawn im crewmate- und scout-Pfad setzt. Ohne den Schalter parkt codex auf dem Hook-Vertrauensdialog ("N hooks are new or changed"), dessen Auswahl auf "Review hooks" aufsetzt und der mit der Tastenebene dieses Werkzeugs nicht beantwortbar ist. Der Lauf meldet dann die Tuerklingel als nicht befolgt, obwohl er ueber die Tuerklingel gar nichts ausgesagt hat - und das auf jedem Checkout, weil schon die projekteigene .codex/hooks.json den Dialog aufzieht. Dazu eine knappe Zusicherung am Startrezept, damit der Schalter beim naechsten Umbau nicht wieder still ausfaellt. Co-authored-by: ladwein <ladwein@firstmate.bost8.thelad.loc>
…kunchenguid#7096) * fix(bin): store only classified review fields in contribution records A pull observation stored GitHub's raw reviews payload, so a PR with a few hundred reviews wrote a record past the 1 MiB read cap and every pending, verdict and ack read then refused the whole home. Project each stored review to id, state, submitted_at, commit_id and user.login, the only fields the classifier reads, and add a many-review regression. * no-mistakes(document): clarify contribution record read cap in script header
…7216) * fix(pi): retain custom-event handling and processing cancellation * fix(pi): identify custom triggers before legacy prompt narrowing * no-mistakes(test): Correct branch fixture retirement and verify real notification ownership * no-mistakes(document): Clarify Pi event boundaries and verification scope * fix(pi): retain custom-event handling and processing cancellation * fix(pi): identify custom triggers before legacy prompt narrowing * no-mistakes(test): Correct branch fixture retirement and verify real notification ownership * no-mistakes(document): Clarify Pi event boundaries and verification scope * no-mistakes(review): Fix SDK1.1 prompt staging and verify branch context * no-mistakes(document): Clarify SDK prompt compatibility and branch-context verification * no-mistakes(test): Require dual-SDK compiler checks; execution remains pending * no-mistakes(document): Correct SDK typecheck documentation and stale dependency claims * no-mistakes(document): Correct stale Pi typecheck documentation and ownership pointers * no-mistakes(ci): Fixed SC2329 in tests/fm-pi-primary-types.test.sh by inlining EXIT cleanup. Also fixed a reproduced macOS npm-prefix/lockfile path failure without changing dependency pins or assertions. Canonical targeted lint, strict TypeScript 5.9.3 checks against Pi 1.1.0 and 1.2.0, and success/failure cleanup checks pass. Both behavior-shard failures were upstream npm ETARGET errors in workflow steps unchanged from base; no workflow/runtime changes warranted. Hosted CI remains for the outer executor * fix(test): reconcile queued processing probes after rebase * no-mistakes(document): Correct Pi compiler-phase documentation and stale CI comments
…guid#6380) * fix(cursor): verify cursor-agent by running it, not by name Cursor's installer writes its `cursor` IDE shim through the ~/.local/bin/cursor -> cursor-agent symlink, which can replace a versioned cursor-agent with a script that only prints "No Cursor IDE installation found" and exits non-zero. That file keeps Cursor's own name and install-tree path, so the name shortcut (and the structural check alone) accepted it and every Cursor spawn launched the broken file instead of the working agent alias. A launch candidate now has to print Cursor's CLI identity under the bounded probe, or carry the structural evidence and run --help successfully. Hosts without a bounded runner keep the structural verdict, as before. * no-mistakes(document): Align cursor launch-verify docs with runtime * no-mistakes: apply CI fixes * test: restore fm-watch-triage ack_stopped_cycle assertion The CI fix round loosened ack_stopped_cycle so a drain that prints no acknowledgement command and exits zero counts as success. That test is unrelated to the Cursor executable verification this branch changes, so restore its original assertion rather than mask a watcher failure here. * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix(bin): restore fm-afk-contract clock sampling A CI fix round changed how fm_afk_contract_cmd_enter samples its epoch. That away-mode behavior is unrelated to the Cursor executable verification this branch changes, so restore it here and leave any fix to its own change. * fix(cursor): load the timeout library only when a probe runs Sourcing bin/fm-timeout-lib.sh at load time made every consumer of fm-cursor-lib.sh, including the session-lock and turn-end hook paths that never probe, depend on that file being next to it. Load it lazily from the probe instead. * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * test: restore herdr focus-flash e2e test A CI fix round edited this Herdr end-to-end test, which is unrelated to the Cursor executable verification this branch changes. Restore it here and leave any fix to its own change. --------- Co-authored-by: ladwein <ladwein@firstmate.bost8.thelad.loc>
…ee (kunchenguid#7752) The EXIT trap removed the fixture root with a plain rm -rf. When the run stops early, for example when the watcher auto-relaunch leg fails, the fixture still holds the read-only state/<id>.git-hooks strip folder, so cleanup prints "Permission denied" for each hook, leaves the fixture root behind and exits non-zero. Use the shared fm_test_remove_tree, which makes read-only fixture directories writable before it removes them. Refs kunchenguid#7629
* feat(bin): add allowlisted live record sync * feat(bin): archive teardown records by generation * feat(bin): export task records before teardown * docs: define the private record export contract * fix(bin): satisfy ShellCheck in the record exporter and its test * fix(bin): publish record exports as safe snapshots * fix(bin): make record archives transactional * fix(bin): fail closed on record export teardown * fix(bin): harden record export transactions * fix(bin): prevent record export staging wedges * no-mistakes(review): Archive task records before irreversible teardown cleanup * no-mistakes(review): Archive records after endpoint quiescence * no-mistakes(document): Consolidate record export architecture documentation * no-mistakes(ci): [Certain] Fixed both CI issues: quoted the remote-home test fixture assignment to clear ShellCheck, and added the missing `spawn_gen` to the export-enabled remote retirement fixture so the expected lost-slot exit code is preserved. ShellCheck, `git diff --check`, and the focused remote-home export test passed. The full lifecycle E2E was blocked locally by an earlier fixture failure (`remote spawn never reached its blocked inheritance write`); CI’s log identified the later failure this fixture correction addresses * no-mistakes(ci): [Certain] Fixed the fixture to replace any existing `spawn_gen` field before setting `remote-retirement-test`, preserving the exporter’s exactly-one-generation invariant. `git diff --check` passed. The full remote lifecycle test could not reach this scenario: it failed earlier at “remote spawn never reached its blocked inheritance write,” and cleanup reported permission errors on generated git-hook files * no-mistakes(ci): [Certain] Fixed the fixture race in `tests/fm-remote-secondmate-lifecycle-e2e.test.sh`: it now pins `spawn_gen` after the blocked respawn finishes publishing metadata, so the exporter receives exactly one generation key. `bash -n` and `git diff --check` passed. The full E2E remains blocked by an earlier failure to reach the blocked inheritance write, followed by cleanup permission errors; it did not reach the corrected assertion, captain * no-mistakes(ci): [Certain] Fixed `teardown_export_records` to return before config-directory safety checks when `record-export-root` is absent, and added a regression test for an unopted home with symlinked `config/`. The regression passed. The full teardown suite stopped in its existing inaccessible-config case: this environment runs as root, which can read the chmod-000 directory, so that case cannot reproduce its expected refusal here. `bash -n` and `git diff --check` passed; `fm-lint.sh` was blocked because `actionlint` is unavailable. The CI log shows steady test progress followed by GitHub cancelling the job and cleaning up orphan processes, with no reported test failure; treat it as provider cancellation and let the fresh push rerun it * no-mistakes(ci): [Certain] Updated the teardown test fixtures: a regular file at the export-root path now deterministically causes export failure, and a symlinked config directory tests the opted-in safety refusal across users, including root. The unopted symlinked-config regression remains intact. The relevant teardown cases passed; `bash -n` and `git diff --check` passed. The full suite later failed an unrelated leaked-process-reap case. Serial 1 was cancelled without a verdict and, per the provided instruction, needs no code change
…is warm (kunchenguid#7807) * feat(bin): add opt-in warm-cache compaction for the Claude primary and host engine Refs kunchenguid#7755 * no-mistakes(review): Persist engine compaction marker, run it abortable, match resume predicate * no-mistakes(review): Run engine compaction in host shell; skip marker on abandon * no-mistakes(review): Test abandoned engine compaction is not marked done * no-mistakes(document): Clarify warm-compact episode and engine resume rules * no-mistakes(lint): Fix ShellCheck SC2015 in engine compaction marker * no-mistakes(ci): Test coverage guard and portable parallel 2 failed because the two new warm-compact tests had no timing hint. The runner packed them at the 45000 ms default, pushing the largest serial shard to 1204066 ms, over the 1200000 ms limit. I added measured hints in bin/fm-test-run.sh (fm-warm-compact.test.sh 1500, fm-warm-compact-herdr-live-e2e.test.sh 170). `bin/fm-test-run.sh --check-coverage` now passes (largest serial shard 1194253 ms), and tests/fm-supervision-instructions.test.sh passes. A full run of tests/fm-test-run.test.sh was killed at its time limit and did not finish, so it is unverified. The serial-shard failures in the logs were watcher/timing tests unrelated to warm-compact. I did not reproduce them locally. main also has a failing run. They need a rerun, not a code change
…7810) * refuse remote Rex servers during spawn * no-mistakes(document): Clarify Rex locality requirements and remote-worker guidance
* fix(bin): read a Windows win:<pid> session lock in the supervision host and Claude auto-arm On Git Bash the session lock records its holder as a tagged win:<pid> (kunchenguid#6300), but four readers still passed that value to the numeric-only fm_pid_identity and fm_pid_alive: fm_supervision_host_main_key produced no key, fm_autoarm_claim_next pinned no lock-holder identity and dropped owner_session_id, fm_autoarm_claim_open rejected the non-numeric lock so a live claim never read as open, and fm_autoarm_midturn_healthy read a live holder as lapsed. Add fm_session_pid_alive and fm_session_pid_identity beside the tagged-pid contract in fm-session-lock-lib.sh. A tagged pid is answered from the Windows process table; every other value falls straight through to the existing numeric helpers, so POSIX behaviour is unchanged. The claim read-back checks the lock shape with fm_session_pid_valid. tests/fm-session-lock-windows-consumers.test.sh covers each reader on a faked Git Bash host, including an exited holder, a reissued Windows pid, and the tag off Windows, plus a parity case proving untagged values get exactly the numeric helpers' answers. Fixes kunchenguid#7739 * no-mistakes(document): Document tagged Windows session-lock consumers * no-mistakes(ci): Fixed tests/fm-afk-return.test.sh partial host fixtures to copy fm-session-lock-lib.sh alongside fm-supervision-engine-lib.sh in all three affected paths. The focused host/latch tests and tests/fm-session-lock-windows-consumers.test.sh pass; the full afk-return suite exceeded the 180s local timeout after earlier cases passed
) * test: fail required watcher-triage cases without tasks-axi * no-mistakes(document): Document required watcher-test dependency and failure rationale * no-mistakes(ci): Rebased onto 2ba4c45 successfully. New HEAD: 5902c09. Preserved all changes in the relocated pause suite. No conflicts or tracked changes remain. Missing-dependency regression, syntax, and diff checks passed. Command guards blocked the installed-dependency test loop and backup cleanup. Untracked .ci-rebase-backup.bundle remains * Untrack pipeline backup bundle * no-mistakes(review): Untrack pipeline backup bundle; exclusion remains blocked * Untrack excluded pipeline backup bundle * no-mistakes(document): Document watcher test prerequisite and remove stale count * no-mistakes(ci): Fixed SC2329 with a one-line annotation for the function exported to child Bash. Tier 3 applies because static analysis cannot trace that indirect use. Focused lint, all four missing-dependency cases, syntax, and diff checks passed
…unchenguid#7875) * fix(scout): authorize task-scoped empty-inventory completion (kunchenguid#103) Allow scouts to run only their generated complete --none command for an empty inventory, without granting metadata or backlog edit authority. Keep non-empty inventories and refused attestations with firstmate. Fork carry for kunchenguid#7862 pending upstream validation. * fix(scout): allow scoped empty-inventory attestation * no-mistakes(document): point lifecycle doc at scout brief boundary test
* Add opt-in reminders for overdue open worker decisions * no-mistakes(document): Align overdue-decision documentation with watcher behavior * no-mistakes(ci): Fixed ci-1 with a tier-1 structural correction: AWK now compares both task and key as exact strings. Added behavioral regressions for keys 1/01 and task IDs 1/01, including independent backoff. Reproduced the failure before the fix. All focused reminder, decision-key and cursor tests passed, as did stock /bin/bash verification, canonical lint and git diff --check. Only the two intended files changed. No scratch files remain. Publication and PR updates remain with the outer executor
* fix(pi): contain repeated successful unchanged primary reads Add an off-by-default Linux Pi primary safeguard with bounded configurable limits, verified ownership, durable stopping, and deliberate user reset. Preserve ignored legacy guards and require explicit collision migration. Cover policy, SDK compatibility, installed RPC behavior, and test selection. Refs kunchenguid#7805. * no-mistakes(test): Fixed locale-dependent coverage checks; coverage and shard packing passed
…uid#7886) * fix(bin): reclaim a lock whose dead holder's pid was reused The generic lock in fm-wake-lib.sh judged a record stale only by asking whether its pid was alive. Once the holder died and the kernel handed that pid to an unrelated process, the lock read as held forever and every acquirer spun and gave up - each watcher re-arm paid for it while trying to append to the cycle ledger. Acquisition now records the holder's start time beside its pid, and a record whose live pid no longer has that start time is stale, so the ordinary stale-owner takeover reclaims it and reports the pid it took over. Start time alone is the key because a holder may exec another program, which keeps its pid and start time but replaces its command line. A record without a start time, or a pid whose start time cannot be read, keeps the old pid-liveness answer. The bounded wait's handoff drops the helper's start time before the pid changes hands and records the caller's after. Refs kunchenguid#7885 * no-mistakes(document): Note reused-pid lock staleness in FM_LOCK_STALE_AFTER
… work (kunchenguid#7352) * feat(bin): add opt-in config/spawn-base-primary-checkout for local landings A fresh pooled task worktree is reset to origin/<default>. A local-only project lands work on the primary checkout's default branch through fm-merge-local.sh and never pushes, so that ref stays frozen at the last push and every fresh task starts behind work already landed, loading the project's instruction files from the stale tree. The always-on fix proposed in kunchenguid#3812 replaced the specified origin-only refresh for every home; this rework keeps that rule as the default and moves the new base behind an explicit, default-off presence flag, following the existing config/<flag> pattern. With config/spawn-base-primary-checkout present: - fm-spawn.sh starts a fresh local-only ship, or a scout without --base-branch, from the primary checkout's <default> when it strictly contains origin's tip. Publishing modes and named base branches keep origin's tip, because that work is delivered against origin. A diverged primary keeps origin's tip and prints a note counting both sides. The commit is read from the local object store, so nothing extra is fetched, and the clean-slot refusal still runs first. - fm-review-diff.sh anchors a remote-backed task without base_branch= on the branch it delivers to: publishing modes stay on origin/<default>, while a local-only ship or a scout is compared against whichever of the local <default> and origin/<default> meets the branch later, so the review shows the task's own work instead of every locally landed commit. - fm-promote.sh tells a scout promoted to a publishing mode to return to origin's default branch, because in this home the scout may have started from the primary checkout's branch, whose unpushed commits would otherwise be published with the work. fm-dod-lib.sh gains fm_mode_publishes as the one predicate those three readers share. Without the flag every path is unchanged: the spawn resets to origin's tip, the review anchors on origin/<default>, and promotion keeps its wording; the existing tests run unmodified. Treehouse 3 (verified on 3.1.3) already hands a slot out at whichever of the local and remote default branch is further ahead, so with the flag the refresh keeps that slot where Treehouse put it for a local landing instead of rewinding it, and a project without origin is untouched as before. Supersedes kunchenguid#3812. * no-mistakes(document): Drop change-relative wording from spawn-base config docs * no-mistakes(ci): The Greptile Review check failed on two review findings, not on a broken build or test. I fixed both as the user directed, and the tests and lint pass. ci-1 (bin/fm-promote.sh): promotion must name origin's default branch only when spawn could actually have started the scout from the primary checkout. Spawn does that only when the project has an origin remote and is recorded on the task. Before this fix, the opt-in flag alone made promotion to no-mistakes or direct-PR say "copy of origin's default branch...", even on a project with no origin. - The new wording now also requires a recorded project (`[ -n "$PROMOTE_PROJECT" ]`) whose `git remote get-url origin` succeeds. That is the same check bin/fm-review-diff.sh already makes. The empty-project guard is needed because `git -C ""` would run in the current directory. Otherwise the existing "default-branch base" wording stays. - The three places that read the flag now agree: spawn, review-diff and promote. - I updated the header and inline comments in fm-promote.sh, and the promote line in docs/configuration.md, to say this applies only to a project with an origin remote. - Test: `test_promote_names_origin_for_a_publishing_mode_in_an_opted_in_home` in tests/fm-task-delivery.test.sh now records a real `project=` with a registry entry, and covers flag on/off × origin/none × direct-PR/no-mistakes/local-only. Only flag on + origin + a publishing mode gets the new wording. The named-base case is unchanged. - Before the fix, the test fails with "not ok - flag on, remote none, direct-PR: the promotion changed the default-branch wording". With the fix, it passes. ci-2 (tests/fm-spawn-pool-base-freshen.test.sh): with the flag set, both publishing modes must start from origin's tip and print no divergence note. - The opted-in spawn test now covers no-mistakes as well as direct-PR, local-only and scout. - The expectations no longer single out direct-PR. Both publishing modes now expect origin's tip, no "note: pooled worktree", and a review anchored at the slot's start, in the leading, diverged and trailing cases. Verification: - `bash tests/fm-task-delivery.test.sh`: exit 0. - `bash tests/fm-spawn-pool-base-freshen.test.sh`: exit 0. - `bin/fm-lint.sh` on the three changed shell files: exit 0 (ShellCheck 0.11.0, pinned, with extended analysis)
…enguid#7915) * fix(teardown): distinguish unavailable content-landed proof * no-mistakes(document): Clarify teardown proof failure documentation * no-mistakes(ci): Captain, CI failed in unchanged watcher recovery code, independent of this PR. The focused failing case passed locally with exit 0. The full suite stalled earlier and was stopped. No tracked changes. Command guard rejected cleanup; scratch remains at .ci-7915/ and tests/.ci-7915-cycle.sh * chore: untrack CI validation scratch
…kunchenguid#7876) * feat(dispatch): prefer configured provider family * no-mistakes(review): drop policy_why field and dead auth fixture from preference tests * no-mistakes(document): docs: record preferred_provider in schema, diagnostics, verification
) * fix: pin local-only landing to its accepted done head Persist the accepted local-only commit in task metadata and refuse landing when the branch or the recorded head changes. Merge the full commit ID under the task control and metadata locks. Closes kunchenguid#7782 * no-mistakes(test): Refuse local landing races with atomic ref transactions * no-mistakes(document): Document pinned local landing and recovery contracts * no-mistakes(ci): Applied only the verified incremental patch and preserved existing repairs. Complete diff matches the supplied candidate hash. Both focused test files, default lint, and source-aware lint passed with exit 0. Evidence retained at /tmp/up7782-local-repair/ci-phase-fresh-*. No commits, pushes, or pipeline-control actions performed; publication remains with the outer executor * no-mistakes(ci): Fixed both SC2031 false positives in tests/fm-merge-local.test.sh with narrowly scoped suppressions. Tier 2 lint correction; runtime changes were unnecessary. Default lint, source-aware lint, all 8 focused tests, and git diff --check passed. No commits, pushes, or pipeline-control actions performed
…henguid#7928) * fix(procevent): coalesce unhandled extension host errors * no-mistakes(document): Document host-error coalescing and recovery semantics
…uid#7912) * fix: stop wake grants when lock acquisition fails * no-mistakes(document): Document wake grant lock acquisition failure behavior
* fix: send remote steers over stdin * fix: version remote stdin send protocol * no-mistakes(document): document stdin transport for remote steer in fm-send header * no-mistakes(ci): Two tests failed in CI. One is caused by this PR; the other is not. 1. Behavior portable serial 5: `tests/fm-secondmate-reconcile.test.sh` failed with "supervision did not deliver the durable reconcile request later". This PR caused it. The PR now sends a remote steer's body on stdin, using `fm-on.sh --stdin` with the `send-stdin` verb. This test's fake ssh started with `cat > /dev/null`, so it threw the body away before it ran the real `fm-remote-secondmate-control.sh`. `cmd_send_stdin` then read an empty message, died with "remote steer message must be nonempty", and no inbox record was written. Rule that broke: a fake ssh that runs the real remote leg must pass the caller's stdin through, as real ssh does. I checked every fake ssh that runs the real leg. `fm-send-remote-delivery.test.sh` already passes stdin through. `fm-secondmate-sync.test.sh` (`make_remote_leg_ssh_stub`) had the same discard line. The stub in `fm-send-resolve-key.test.sh` never runs the leg, so it is fine. The fix deletes the `cat > /dev/null` line in the reconcile and sync stubs. Calls made without `--stdin` still get /dev/null from fm-on, so nothing else changes. Checks run locally: the reconcile test fails before the fix with the same CI message. After the fix, the reconcile test passes (rc=0) and the sync test passes (rc=0). 2. Behavior portable serial 3: `tests/fm-wake-queue.test.sh` failed in `test_concurrent_append_and_drain` with "wake drain: queue lock could not be acquired safely". This PR did not cause it. The PR does not touch `bin/fm-wake-drain.sh`, the wake tests or the wake helpers. The failure is lock contention: 40 appenders run at the same time as a drain, on a loaded CI runner. Locally the whole test passed (70 ok, rc=0). I made no change for it. It is an existing flake in wake-queue lock acquisition and needs its own fix outside this PR. Open PR kunchenguid#7912 ("preserve wake grants when queue-lock acquisition fails") works in the same area
…owner manages (kunchenguid#7357) * fix(herdr): never start a server an external owner manages or is replacing fm_backend_herdr_server_ensure started a detached herdr server whenever status reported the session not running. When a service manager owns the server, that races a unit restart or a server live-handoff and splits panes across two servers. The session directory reported by herdr session list can now carry a firstmate-server-owner marker (persistent, naming the owner) and a firstmate-server-replacing marker (for the duration of a restart or handoff). While either exists and the server is not running, server_ensure never starts a server: it waits a bounded time (FM_BACKEND_HERDR_EXTERNAL_SERVER_POLLS, default 30s) for the server to report running, and otherwise fails naming the owner or the replacement. A running server is still used at once, and a session with no marker keeps the Firstmate-owned autostart unchanged. Fixes kunchenguid#7320 * no-mistakes(review): Enforce herdr replacement marker on fast path; fixed wait bound * no-mistakes(ci): Both CI failures had one cause in this PR, and I fixed it in code; no test assertions changed. `fm_backend_herdr_server_ensure` had started running `herdr session list --json` on every call to find the session directory for the ownership markers. Two existing tests treat that call as a sign of something else: - `tests/fm-control-relaunch.test.sh` failed with "a herdr reclaim touched a session the record does not name: session list --json", because it allows only `--session fmlab` calls and `status --json`. - `tests/fm-backend-herdr-presentation-e2e.test.sh` failed with "opted-out spawn added presentation-ordering capability or socket calls", because there `session list` means presentation ordering ran. Invariant: `server_ensure` must not make Herdr calls beyond its own `status` read, so ordinary spawns and reclaims stay free of `session list` and only touch the session they name. Every caller goes through `server_ensure`, so fixing it there covers all of them. Fix (`bin/backends/herdr.sh`): the session directory now comes from the `status --json` read `server_ensure` already makes. It is the directory containing `.server.socket`. I removed the `fm_backend_herdr_session_dir` helper. On a running server, the only added work is one check for the replacement marker, which is what the user asked for. I checked this against real Herdr 0.9.5: - `status` reports the socket even when the server is stopped, for both the default and named sessions. - The socket's directory is the same `session_dir` that `session list` reports, and it follows `XDG_CONFIG_HOME`. - With `--session`, an inherited `HERDR_SOCKET_PATH` does not change it. - The read creates nothing on disk. If a Herdr version omits the socket, `server_ensure` keeps the old Firstmate-owned behavior, as the docs already said for a missing `session_dir`. Other changes: the marker-test fakes now report the socket under the session directory, and I removed the `session list` stub this PR had added to the shared fake. `docs/herdr-backend.md` and `docs/verification/runtime-backends.md` ("Session directory") now describe and record the `status`-socket measurement. Verification: - `tests/fm-backend-herdr.test.sh` passes: 239 ok, 0 not ok, including all the externally-managed and replacement-marker tests. - `tests/fm-control-relaunch.test.sh` passes; it was the shard 3 failure. - shellcheck shows only existing info-level notes. Not verified locally: the real-Herdr presentation test. Here it stopped earlier, at an unrelated Treehouse timeout ("treehouse get did not enter an isolated worktree within 60s"). After the fix `server_ensure` no longer runs `session list`, which is the call that assertion caught. No lab session was left running, and I removed the test's temp directory
Brings in the 146 upstream commits since e0d95ac while keeping every fork feature: the executor lane, the bounded Claude auto-compact window (config/crew-autocompact), the lighter fork CI, and the orphan-reparenting test fixes. Integration fixes beyond the textual conflict resolutions: - ci.yml: the macOS job carried two `if:` keys after git's auto-merge; it now has one condition that ANDs the fork's repository gate with upstream's FM_CI_SKIP_PUSH and FM_CI_SKIP_MACOS opt-outs, as every gated job does. - fm-ci-workflow test: the fork's repository-gate checks now run through upstream's GitHub-expression evaluator, which gains github.repository. - fm-spawn --executor refuses the postures and surfaces its headless forms were never verified on: codex under a non-bypass approval or permission mode, a detected OpenCode 2, backends without a verified pane shell (t3code, paseo, rex, tern), projects registered with a forge other than none, and --upstream-consent; a firstmate-repository executor passes the self-repository publication gate under this home's maintainer scope. - The opencode executor launch runs the same resolved executable the version gate probed (__OPENCODEBIN__), as the interactive launch now does. - Executor tests follow upstream's reshaped Git config prefix and stub opencode so the launch does not depend on the host's installed binary.
…de autocompact limit
…portable serial 7" check; the change is only in tests/fm-omp-harness.test.sh. Of the 27 scripts in that shard, only this one failed. Its case "omp watch extension gate (off)" passed locally on every run, so this was a timing flake under CI load, not a bug in the merge. **Cause:** the test calls `fm_watch_arm_omp`, which returns as soon as it has spawned the watcher process. The test then waited at most 6 s for the log file to appear and read it the moment it did. The fake watcher writes its log line with `printf ... >> "$FM_ARM_LOG"`, which creates the file before the line lands. On a slow runner the test either read an empty file or ran out of time first. Either way it saw no log lines, which matches the empty list in the CI error. **Rule it broke:** a bounded wait must wait for the complete event it checks, and its deadline must outlast a slow CI spawn. **Fix:** - The gate case now polls until the log holds a whole newline-terminated line, and empty entries are dropped from what it reads. - I raised all four 6 s polls in the file to 30 s, which matches the extension's own `hostReadyTimeoutMs`. Each loop exits as soon as its condition holds, so the longer limit only matters when a test is really failing. The other three wait on an in-process count that updates in one step, so they only needed the longer limit. **Verification:** I forced the race by temporarily changing the fake watcher to create the log, sleep 7 s, then write. The original test failed exactly as CI did ("gate (absent)" not ok, empty list). The fixed test passed. The temporary edit was removed. A clean `bash tests/fm-omp-harness.test.sh` passes all 19 cases. shellcheck shows only existing info-level SC1091/SC2153 notes on lines I didn't touch
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.
Brings the fork up to date with upstream's 146 commits while keeping all 27 fork commits: the executor lane, the bounded Claude auto-compact window (
config/crew-autocompact), the lighter fork CI (#5), and the orphan-reparenting test fixes.Conflicts and resolutions
.agents/skills/harness-adapters/references/harness/claude.md.agents/skills/operational-home-layout/SKILL.mdclaude-permission-modeandcodex-approval-modelines with the fork'screw-autocompactline between them..github/workflows/ci.ymlubuntu-24.04runners; each gated job'sif:ANDs the fork's repository gate (github.repository == 'kunchenguid/firstmate', or a pull request for the portable lanes) with upstream'sFM_CI_SKIP_PUSH/FM_CI_SKIP_MACOSopt-outs.tests/fm-ci-workflow.test.shgithub.repository, replaces the fork's mini-evaluator; the fork's repository-gate cases and upstream's variable table both run through it.bin/fm-test-run.shdocs/scripts.mddocs/architecture.md,docs/trace-context.mdbin/fm-brief.sh--issue/--verifyand--upstream-consent/--project); the brief-level self-publish pre-gate stays ship-only as upstream had it.bin/fm-config-inherit-lib.shcrew-autocompact.bin/fm-teardown.shremove_harness_worktree_wiringand t3code cleanup are kept, and.fm-pr-body.mdis removed on both paths.bin/fm-watch.shdocs/configuration.mdbin/fm-spawn.shtests/fm-control-relaunch.test.shfm_test_cleanup; both sides' test lists.tests/fm-secondmate-harness.test.shtests/fm-spawn-dispatch-profile.test.shfm_test_launch_git_prefixwith the fork's auto-compact segment; both sides' tests.tests/fm-watch-triage.test.shIntegration fixes (semantic breaks git did not flag)
ci.ymlmacOS job had twoif:keys after git's auto-merge, a duplicate mapping key that loses one of the two conditions; it now has a single combined condition.fm_role_gate_self_publishunder the home's maintainer scope;--upstream-consentis refused for--executorbecause that one-task consent route is ship-only.codex execis verified only with the bypass flag, so--executor --harness codexrefuses underconfig/codex-approval-mode=approve-for-meorconfig/codex-permission-mode=restrictedrather than silently overriding the captain's posture.--executor --harness opencoderefuses it; the executor launch now runs the same resolved__OPENCODEBIN__the version gate probed.--executoraccepts only tmux, herdr, zellij, orca, and cmux and refuses t3code, paseo, rex, and tern.forge=other thannone(external, gerrit, forgejo) refuses--executorinstead of bypassing that forge's review path.fm-spawn-executorfollows upstream's reshaped Git config prefix and stubs opencode so the launch does not depend on the host's binary, plus a new test pinning every refusal above; the relaunch executor fixture gets the same stub.docs/configuration.md"Executor launch" and thefm-spawn.shheader record the new refusals.tests/fm-omp-harness.test.sh"omp watch extension gate" waited at most 6 s for the spawned arm's log and could read it before the line landed, so it failed once under CI load; its polls now wait up to 30 s for a whole line, with the assertions unchanged.fm-approval-failover.sh applyon an executor would have saved the fallback home-wide and then failed to recover the worker, sincefm-control.shcannot interrupt a one-shot executor; it now refuseskind=executorbefore writing anything, with a regression test and an operator-docs sentence.docs/t3code-backend.md"Active limits",docs/configuration.md"Claude auto-compact window", and thefm-spawn.shheader now say so.Validation
Ran once each in the foreground: the tests beside every conflicted or fork-side script (fm-spawn-executor, fm-spawn-dispatch-profile, fm-spawn-, fm-control-relaunch, fm-control, fm-teardown, fm-test-run, fm-ci-workflow, fm-secondmate-harness, fm-watch-triage*, fm-watch-open-decisions, fm-brief*, fm-executor-, fm-crew-state, fm-send-, fm-classify-, fm-claude-trust, fm-dod-lib, fm-bootstrap, fm-self-repo-role, fm-approval-failover, fm-shared-captain-inheritance, fm-dispatch-resolve, fm-trace-context-lib), all green;
bin/fm-lint.shandbin/fm-doc-audience-check.shclean.The no-mistakes pipeline then ran review (two fix rounds), test, and document. Its lint step crashed on the validating host's
LC_NUMERIC=sv_SElocale before linting anything, a pre-existingfm-lint.shissue left out of this diff.bin/fm-lint.shunderLC_NUMERIC=Con the pipeline head was clean, so the step was approved, and CI's lint job checks it again.Upstream commits merged (146)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
bin/fm-approval-failover.sh:62- The upstream approval-failover script and the fork's executor lane don't know about each other. A codex or claude executor's meta recordsapproval_mode=bypass(bin/fm-spawn.sh:7162-7163), sofm-approval-failover.sh apply <executor-id>passes every guard and permanently writesconfig/codex-approval-mode=approve-for-me(orconfig/claude-permission-mode=auto) home-wide. It then callsfm-control.sh <id> interrupt, which refuses for kind=executor (bin/fm-control.sh:110-112), so it ends with 'fallback saved, but worker recovery failed'. Concrete sequence: acodex execexecutor under a managed policy hits an approval prompt or refusal, firstmate gets the captain's OK and runs failover. Result: the executor is not recovered, and the saved setting makesexecutor_refuse_non_headless(bin/fm-spawn.sh:3209-3214) refuse every later codex executor spawn and the relaunch of this task, until someone deletes the file by hand. Fix: refusekind=executorin fm-approval-failover.sh before anything is written, next to the existing t3code refusal at line 66, pointing the operator to re-dispatch on another headless adapter or as a ship task.docs/configuration.md:1340- The fork's bounded auto-compact feature does not reach upstream's new t3code backend. The docs (docs/configuration.md 'Claude auto-compact window'; the bin/fm-spawn.sh header) say--autocompactapplies to every interactive Claude worker launch. On backend=t3code, though, T3 owns the Claude command line and__CLAUDEAUTOCOMPACT__is only substituted into the pane launch template (bin/fm-spawn.sh:2902, 7366). So t3code Claude workers silently run on Claude's own default, with or without config/crew-autocompact. They do not get the 200000 default either, and docs/t3code-backend.md 'Active limits' doesn't mention it. Upstream's pattern for other settings T3 can't honor is to refuse or document them. Refusing here would block every t3code Claude spawn, because an absent file means 200000. This needs a product call: document the limit in t3code-backend.md and configuration.md, or carry the window through T3's per-worktree Claude settings env.🔧 Fix applied.
2 issues (1 warning, 1 info) still open:
docs/configuration.md:1340- The fork's bounded auto-compact feature does not reach upstream's new t3code backend. The docs (docs/configuration.md 'Claude auto-compact window'; the bin/fm-spawn.sh header) say--autocompactapplies to every interactive Claude worker launch. On backend=t3code, though, T3 owns the Claude command line and__CLAUDEAUTOCOMPACT__is only substituted into the pane launch template (bin/fm-spawn.sh:2902, 7366). So t3code Claude workers silently run on Claude's own default, with or without config/crew-autocompact. They do not get the 200000 default either, and docs/t3code-backend.md 'Active limits' doesn't mention it. Upstream's pattern for other settings T3 can't honor is to refuse or document them. Refusing here would block every t3code Claude spawn, because an absent file means 200000. This needs a product call: document the limit in t3code-backend.md and configuration.md, or carry the window through T3's per-worktree Claude settings env.docs/configuration.md:1441- Leftover from the round-1 fix. Round 1 added thekind=executorrefusal at bin/fm-approval-failover.sh:72-75 and noted it in the script header. The operator section 'Persistent managed-policy failover' in docs/configuration.md still lists only thebackend=t3coderefusal (line 1441), so an operator reading the docs would expect failover to work on an executor task. Fix: add one sentence after line 1441 saying akind=executortask refuses before any change, because fm-control cannot interrupt or relaunch a one-shot executor; re-dispatch it on another headless adapter or as a ship task. The rest of the round-1 fix is correct: the guard runs before any config write or lifecycle call, and the new test drivesapplyon an executor record and checks for exit 1, an untouched config dir, and no control calls. It would fail without the guard.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Minted a lab withbin/fm-lab-home.sh create, a private tmux socket (tmux -L fm-lab), a treehouse pool inside the lab, and a throwaway git project with a local bare originbin/fm-spawn.sh exec1 $FM_HOME/projects/demo --executor --issue 1 --yolo off --harness claude --model haiku: started a real claude -p executorbin/fm-approval-failover.sh apply exec1against the real executor taskbin/fm-spawn.sh ship1 ... --mode direct-PR --yolo off --harness claude --model haiku: started a real interactive Claude worker, and I read its argvbin/fm-approval-failover.sh apply ship1: ran a real save, interrupt and relaunchbash tests/fm-approval-failover.test.sh,tests/fm-spawn-executor.test.sh,tests/fm-executor-poll.test.sh,tests/fm-spawn-dispatch-profile.test.sh,tests/fm-claude-autocompact-live-e2e.test.sh,tests/fm-executor-headless-flags-live-e2e.test.sh,tests/fm-watch-triage.test.sh,tests/fm-backend-herdr.test.sh,tests/fm-control-relaunch.test.shLab teardown (tmux kill-server, fm-lab-home teardown, rm -rf); worktree confirmed clean✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.