Repository navigation
merge: sync fork with upstream kunchenguid/firstmate@2911a3bb - #9
Merged
Merged
Conversation
…es (kunchenguid#5863) * fix(pi): silence unacknowledged processing retry replies Suppress autonomous processing prose before persistence and during streaming while retaining tool calls, signed reasoning, and retryable outcomes. Restore ordinary output after acknowledgement or a user message. Fixes kunchenguid#4954 * no-mistakes(review): Silence only processing retries, keep first presentation visible * fix(pi): preserve differing processing retry replies
…henguid#6530) Pi 1.0.1's createToolHtmlRenderer reads getToolRenderers and ignores getToolDefinition. Calm /export still includes stock grep HTML; the fixture has to pass the lookup key the installed Pi actually reads.
* fix(procevent-quota): tolerate consecutive slow quota-axi reads The quota allowance poll treated any quota_json failure as terminal, so one slow quota-axi --json (measured max ~29s under a 48s derived bound) shut the watch down until someone re-armed it, and the detail always said "missing/incompatible". Tolerate three consecutive failed or timed-out reads before going terminal, reset the streak on any good read, and report a timeout distinctly from a missing or incompatible tool. Each timed poll runs exactly one bounded --version and one bounded --json: validate the captured version text through fm_quota_axi_version_compatible rather than launching a second probe, and describe a mixed failure streak by count plus last cause. * no-mistakes(document): Document quota polling failure tolerance * no-mistakes(ci): Fixed ci-2 and ci-3. Permanent quota read failures (rc 2 missing, rc 3 incompatible) now report on the first poll, while transient rc 1/4 failures retain the existing three-failure retry behavior. The missing-binary test now uses an isolated PATH without quota-axi and asserts both permanent failures stop at condition_polls: 1. Verification passed: tests/fm-procevent-quota.test.sh, canonical fast lint for both changed files, bash syntax checks, and git diff --check * no-mistakes(review): Classify untimed quota version failures as transient * no-mistakes(document): Clarify quota polling failure budget
…enguid#6505) * fix(teardown): clarify scratch guidance and dirty worktree refusals Keep ship proof material outside the task worktree and distinguish untracked-only leftovers from tracked edits without changing cleanup guards. Fixes kunchenguid#6319 * fix(ci): Fixed ci-3 only. Both promotion outputs now replace the scout restriction and require external scratch storage and a clean worktree before done. Verification: 27 delivery tests passed, five mutations caught, restored test passed, pinned ShellCheck and syntax/whitespace checks passed. ci-1, ci-2, and ci-4 remain untouched
…nguid#6518) * Escalate inbox instructions stuck behind a busy worker Count consecutive busy-deferred due doorbells durably and escalate at the configured bound without typing into the worker pane. Fixes kunchenguid#6445 * fix(review): Fix inbox escalation deduplication and busy streak resets * fix(review): Preserve busy inbox escalations through daemon supervision * fix(document): Correct busy-inbox escalation documentation * fix(ci): Fixed SC2034 in tests/fm-task-inbox.test.sh by including the loop counter in the failure diagnostic. Source-aware lint, all 34 inbox tests, and git diff --check pass. Behavior portable serial 6 reproduces identically on base 1f3e769 and target 78156b8 with Pi 1.0.1: an unrelated renderer API change breaks the unchanged Calm test. No Calm changes made; that failure is addressed separately by kunchenguid#6516. Logs retained in scratchpad-ci/ * fix(ci): Fixed ci-2, ci-3, and ci-4: successor failures surface, reset alerts deduplicate, and oversized busy limits fall back to two. Passed 47 inbox tests, 7 focused daemon checks, all 13 mutation checks, lint, documentation checks, and diff checks. Evidence: scratchpad-ci-selected/summary.json. ci-1 remains unchanged and unwaived. Fresh live Herdr proof remains with the outer driver
* fix: prevent healthy remote job worker turnover * no-mistakes(review): Serialize full LaunchAgent repair and verify launchd-tracked owners * no-mistakes(review): Let launchd-tracked unpublished spawns start before reloading * no-mistakes(document): Document remote worker heartbeat and serialized LaunchAgent recovery * no-mistakes(ci): Full CI log showed the idle-worker regression exceeded its outdated command budget (82 versus 80) after independent heartbeat ownership checks were added. Raised the budget to 120 while retaining the separate busy-poll sleep limit. Remote-job and LaunchAgent executable tests passed, as did bash syntax validation and git diff --check. No production behavior changed * no-mistakes(ci): Fixed missing readiness recovery under verified live ownership, preserving the serving PID and private file mode. Added executable regressions for deletion during a blocked sweep and stale readiness diagnostics without LaunchAgent reload. Deletion regression failed before the fix. Both remote-job test suites, bash syntax validation, and git diff --check passed * no-mistakes(ci): Full CI log identified a flaky ownership-loss test racing an already-authorized heartbeat refresh. Replaced backdating and a fixed sleep with bounded observation of readiness expiry through the public probe. Production behavior unchanged. Remote-job and LaunchAgent executable suites passed; bash syntax validation and git diff --check passed * fix: wait for launchd bootout cleanup * no-mistakes(document): Document remote worker recovery and read-only turnover verification * no-mistakes(review): Publish worker identity before lock owner records * no-mistakes(document): Document worker identity publication safety invariant * no-mistakes(ci): Fixed ci-1: replacement workers discard predecessor readiness before publishing identity and roll back identity if lock-owner recording fails. Added executable regressions reproducing both defects. LaunchAgent and orphan-reap suites, shell syntax checks, and git diff --check passed. No live service state was modified * no-mistakes(ci): Restored lock-owner-before-identity publication and removed the identity rollback and reordering-only tests. Retained an executable regression proving predecessor readiness is rejected until replacement startup completes. LaunchAgent and orphan-reap suites, shell syntax checks, and git diff --check passed. Other changes remain intact; the retained-identity interrupted-repair edge remains out of scope. No live service state was modified
* fix(remote-job): reap expired seq claims with one directory walk The hourly claim sweep forked uname+stat per .seq-claims entry and blocked serving for ~85s at ~17k dirs. Delete expired empty claim dirs with a single find -exec rmdir batch and cache the host uname for remaining mtime reads. * no-mistakes(review): Restore original path mtime helper and drop uname cache * no-mistakes(test): Restore claim retention eligibility; focused retention and serving tests pass * no-mistakes(document): Document single-walk claim cleanup and regression entrypoints * no-mistakes(ci): Fixed Lint 2’s reproduced SC1091 by adding the tests/lib.sh ShellCheck source directive to the retention test. Runtime behavior is unchanged. ShellCheck passed for both new claim tests; Bash syntax, the retention behavior test, and git diff --check passed * fix(tests): keep fixture registries out of git worktree roots A TMPDIR pointed at a repository root placed live .fm-test-* registries beside tracked files, and a concurrent git add during the claim-walk CI fix round committed three of them. Route registries and fixture roots through a TMPDIR that refuses git worktree roots, remove the stray files, and pin the escape with a behavioral cleanup test. * no-mistakes(review): Preserve whole-second claim expiry in single-walk sweep * no-mistakes(document): Correct temporary-directory resolution documentation * no-mistakes(ci): Fixed ci-3 by changing only the stale, fresh, and read-only orphan fixture paths in tests/fm-test-fixture-cleanup.test.sh to use $FM_TEST_TMPDIR. All seven tests passed both normally and with TMPDIR set to the worktree root. Shell syntax and git diff --check passed
…t-in (kunchenguid#5354) * feat(bin): record each task's no-mistakes pipeline spend at cleanup no-mistakes keeps every pipeline agent invocation's token usage only in its local agent_invocations records, and cold pipeline agents (review, test, document) leave no session log, so a task's review-loop cost never reached Firstmate's records and could not be attributed after cleanup. bin/fm-pipeline-spend.sh attributes a task's runs by the repository no-mistakes resolves for the task copy, the task branch, and the branch's creation (a relaunch mints a new spawn_gen while the branch keeps validating), then sums every invocation, failed and cancelled included. Token fields use no-mistakes' per-round deltas so resumed review rounds are not counted twice, and unrecorded values stay unknown rather than zero. show prints the record read-only; record appends it once per task incarnation to data/pipeline-spend.jsonl. Teardown records it for every ship task it cleans up, before deleting the branch and the task record. fm_nm_state_db becomes the one owner of where no-mistakes' state database lives, shared with the capped run-inventory reader. * no-mistakes(review): Drop spend show command and timeout override * style(bin): rewrap fm-pipeline-spend header comment * Make pipeline spend recording opt-in * feat(bin): record each task's no-mistakes pipeline spend at cleanup no-mistakes keeps every pipeline agent invocation's token usage only in its local agent_invocations records, and cold pipeline agents (review, test, document) leave no session log, so a task's review-loop cost never reached Firstmate's records and could not be attributed after cleanup. bin/fm-pipeline-spend.sh attributes a task's runs by the repository no-mistakes resolves for the task copy, the task branch, and the branch's creation (a relaunch mints a new spawn_gen while the branch keeps validating), then sums every invocation, failed and cancelled included. Token fields use no-mistakes' per-round deltas so resumed review rounds are not counted twice, and unrecorded values stay unknown rather than zero. show prints the record read-only; record appends it once per task incarnation to data/pipeline-spend.jsonl. Teardown records it for every ship task it cleans up, before deleting the branch and the task record. fm_nm_state_db becomes the one owner of where no-mistakes' state database lives, shared with the capped run-inventory reader. * no-mistakes(review): Drop spend show command and timeout override * style(bin): rewrap fm-pipeline-spend header comment * Make pipeline spend recording opt-in * no-mistakes(document): Note pipeline-spend opt-in gate in teardown comments * no-mistakes(ci): I fixed all three Greptile findings following your decision. Both test suites pass: tests/fm-teardown.test.sh (105 ok, exit 0) and tests/fm-pipeline-spend.test.sh (8 ok). I didn't run either new test against the old code, so the claim that they fail before the fix is from reading the code, not a run. Nothing was committed or pushed. **ci-1 / ci-2 (bin/fm-teardown.sh):** The rule is that every owned ship task in a home that has opted in gets a durable spend record before its task record is deleted. The `[ -d "$WT" ]` check skipped the recorder when the worktree folder was already gone, so no record was written. I removed that check and kept the other conditions: the task must be a ship task, teardown must own its worktree, and `config/pipeline-spend` must exist. `bin/fm-pipeline-spend.sh` already writes an unavailable-source line when the worktree is missing. The new test `test_teardown_records_unavailable_spend_for_a_gone_worktree` in tests/fm-teardown.test.sh sets up an owned ship task whose worktree is missing, with recording turned on. It checks that teardown succeeds, writes a line with `source=="unavailable"`, `total==null` and a reason saying the task copy is gone, and still removes the task record. Under the old check no ledger would have been written. **ci-3 (bin/fm-nm-run-lib.sh):** When `NM_HOME` and `HOME` were both unset, `fm_nm_state_db` fell back to `${HOME:-}/.no-mistakes`, which resolves to `/.no-mistakes`. It now uses `root=~/.no-mistakes`. Bash expands `~` to the account's home directory even when `HOME` is unset, which matches the previous Python `Path.home()` lookup. The new test `test_state_db_without_nm_home_or_home_uses_the_account_home` in tests/fm-pipeline-spend.test.sh runs the function with both variables unset and compares the result to the home directory from the system's password database. The old code would have returned `/.no-mistakes/state.sqlite`. shellcheck reports nothing new on the changed files. I added one `disable=SC2016` comment in the new test, where the single-quoted `$1` is meant to expand in the child shell
* feat(bin): start tasks from a named base branch Spawns always reset a task's pooled copy to origin's default branch, so work that belongs on a feature, integration, or release branch started from the wrong code and opened its PR against the default. fm-brief.sh --base-branch records the base in the brief, fm-spawn.sh resets the copy to origin/<base> and records base_branch= in task meta, and the worker targets its PR at that branch. Review diffs, the cleanup content check, and scout promotion read the recorded base. local-only and Gerrit deliveries refuse a named base. * no-mistakes(review): Anchor brief base-branch parse on scaffold Setup line * no-mistakes(review): Require explicit --base-branch on spawn, matching brief lines * no-mistakes(document): Note base-branch reset in fm-spawn freshness header * no-mistakes(ci): I fixed all four Greptile findings the way you specified. The affected suites pass locally: fm-brief, fm-dod-lib, fm-spawn-pool-base-freshen, fm-review-diff, fm-teardown and fm-task-delivery. shellcheck on the changed files reports only info-level notes, no warnings or errors. I didn't revert the code to watch each new test fail before the fix. - **ci-1 (brief text blocked or redirected spawns):** In bin/fm-dod-lib.sh, `fm_brief_base_branches` now only accepts a `Base branch: <value>` line that comes directly after the base-variant Setup sentence fm-brief.sh writes. Any other `Base branch:` line is ignored. `--base-branch` is still the only authority, and the existing checks in fm-spawn.sh are unchanged. With the flag, the spawn is refused unless a matching pair exists and no pair names a different branch. Without the flag, it is refused only if such a pair exists. The header comment in fm-spawn.sh is updated to match. - The `brief_with_base` test helper now writes the real two-line pair. - The decoy refusal case now uses a full decoy pair in the captain's intent. - New test `test_prose_base_branch_line_is_ignored`: a brief with no base whose intent quotes `Base branch: release/1.2` spawns normally with no `base_branch=` recorded. A spawn with `--base-branch feature/hub` whose intent names release/1.2 starts from `origin/feature/hub` and records it. - **ci-2 (scout base not checked against the forge):** fm-spawn.sh now looks up the project's registered forge with `bin/fm-project-mode.sh --forge`, like the ship path does, before `fm_base_branch_valid` runs. Previously it passed `none`. New test `test_scout_base_branch_refused_on_gerrit_forge`: a scout with a base on a forge=gerrit project is refused at spawn and no meta file is written. - **ci-3 (base not shell-quoted in worker commands):** `fm_dod_block` now quotes the base with `printf %q` in the `--base` and `--base-branch` arguments, and the branch name in the prose stays readable. The test in fm-brief.test.sh uses the base `release/$HOTFIX` and checks that both direct-PR and no-mistakes briefs render `release/\$HOTFIX` in the commands. - **ci-4 (ambiguous PR sentence):** The direct-PR sentence now reads "open a PR with `gh-axi` that is ready for review, not a draft, against the base branch `X` (`--base X`), not the repository default." A brief with no base renders exactly as before. The existing assertion in fm-brief.test.sh is updated * no-mistakes(ci): Both real failures on PR 6442 (run 37064756768) come from CI tooling and a Pi version bump, not from the base-branch feature. Fixes for both already exist on upstream main, so I applied those two commits to the working tree with `git cherry-pick --no-commit`. Nothing is committed or pushed, and no feature file changed. - **Lint 2:** ShellCheck ran out of memory on `bin/fm-teardown.sh` (rss about 8 GB, exit 251, "shellcheck: out of memory"). The rule that broke is that the lint gate must finish on any valid shell root without exceeding the runner's memory ceiling. Upstream commit d719ef3 (kunchenguid#6443) fixes this in `bin/fm-lint.sh`: a root that hits the memory ceiling is retried without `--external-sources`. It also updates `tests/fm-lint.test.sh` and `docs/fm-test-portable-shards.md`. - **Behavior portable serial 6:** `tests/fm-calm-pi-extension.test.sh` failed with "grep disappeared from /export calm.html HTML while calm mode was on". Pi 1.0.1 renamed its HTML export renderer lookup, and upstream commit fede619 (kunchenguid#6530) updates the test to accept the new name. This change is test-only. Both commits applied cleanly. The four touched files are `bin/fm-lint.sh`, `docs/fm-test-portable-shards.md`, `tests/fm-lint.test.sh` and `tests/fm-calm-pi-extension.test.sh`. None of them is in the feature diff, so the feature contract is unchanged. **Local checks:** - `tests/fm-lint.test.sh` passes. - `bin/fm-lint.sh` on `bin/fm-teardown.sh`, `bin/fm-spawn.sh` and `bin/fm-dod-lib.sh` exits 0 with full ShellCheck analysis. - `tests/fm-calm-pi-extension.test.sh` exits 0 with 7 ok and 0 not ok. One case skipped because the Pi package isn't installed locally, so the Pi 1.0.1 export path that failed in CI couldn't be reproduced here. CI needs to confirm it. - I didn't reproduce the 8 GB OOM locally. The PR has to be updated through the pipeline with a normal push: no force push, no replacement PR, no merge
…ole (kunchenguid#6647) * fix(bin): point project workers at the Firstmate skill file The Skill tool cannot resolve a Firstmate skill from another project's worktree, so the launch role names the readable skill file instead. * no-mistakes(review): fix(bin): name Firstmate skill file as fallback only
…ne (kunchenguid#6655) * feat(bin): retire a contribution whose forge object is permanently gone Add fm-contributions.sh retire <task> <url> <captain|fleet> <reason>, which records actor, reason and time on the saved record and removes that task/url pair from known, poll rotation and coverage even while a backlog link remains. Repeating a retire keeps the first provenance; an unrecorded pair, unknown actor, empty reason or unacknowledged pending signal is refused. * no-mistakes(review): Keep retirement per task when settling final owners * no-mistakes(ci): Made the three Greptile fixes the captain chose. ci-1, retirement needs the captain's word: `retire` now accepts only the actor `captain`. Any other actor is refused, including `fleet`. I removed `fleet` from the usage line, the script header (`bin/fm-contributions.sh`), the check in `bin/fm-contributions.sh` and the retired-record validation in `bin/fm-contributions.jq`, which now requires `.actor == "captain"`. The script header now says a retirement always records the captain's word, because the script cannot verify who runs it and the authority to retire is the captain's. The Bearings skill line in `.agents/skills/bearings/SKILL.md` now says `retire` records the captain's word. In the tests, a `fleet` retire is now a refusal case and must leave the record unretired. Every other retire call in the tests uses `captain`. The PR description has not been changed yet: the ruling asks for the same captain's-word statement there, and that belongs to the PR phase. ci-2, blank reasons: the reason check is now `[ -n "${5//[[:space:]]/}" ]`, so a reason made only of spaces or tabs is refused. I added a refusal test that passes a space-tab-space reason. The header also lists a blank reason among the refusals. ci-3, the gone-object test: the test forge wrapper has a new `not-found` fault that returns HTTP 404 for every `api repos/o/r/...` read. The happy-path test `test_retire_ends_observation_of_a_gone_contribution` now uses it in place of the 502 `down` fault, so it models a permanently gone repository rather than an outage. Verification: `bash tests/fm-contributions.test.sh` passed with exit 0. The last lines of its output include the three retire tests passing and no failures. `shellcheck` flagged only SC2034 (`FM_WAKE_QUEUE` unused) and SC1091 (an unfollowed source of `bin/fm-path-lib.sh`), neither on a line this round changed
* test: give the remote-reply whole-log recapture a longer wait * no-mistakes(review): Extend both recapture waits and simplify retry handling
…nguid#6654) * fix(bin): reopen a pending-reply escalation after its resolve A retry of an open decision still appends nothing. A later same-kind escalation after a resolved line for that key appends again, so the next loss is visible. * no-mistakes(ci): Both Greptile findings are fixed in the working tree; the four directly affected test suites pass, and a wider run of related suites had not finished when I returned this result. Invariant: a line already in the status file is recorded, and only a caller with its own evidence of a new episode may append it again. It must hold for every caller of status_event_recorded: the continuity break and the other call in bin/fm-procevent-remote-reply.sh, fm_parent_channel_append_once, and the pending-reply escalation. Changes: - bin/fm-classify-lib.sh: status_event_recorded is back to the idempotent retry check. It returns at the first matching line, and a later resolved line no longer makes that line look new. This fixes ci-1 for every caller and restores the early exit for ci-2, with no separate scan optimization. - bin/fm-pending-reply-lib.sh: _fm_pending_reply_maybe_escalate_locked now owns the reopen. It appends a new blocked line when the record already has an escalated_epoch (it escalated before and was reset) and the keyed decision is no longer open. Otherwise it uses status_event_recorded, so a retry or a resend that leaves the decision open appends nothing. - Comments on status_event_recorded and the pending-reply header describe this scope. Tests: - tests/fm-pending-reply.test.sh: the regression for escalate, operator resolve, second escalate is unchanged and passes. - tests/fm-remote-reply.test.sh: new regression drives the real reader. After an operator resolve of remote-reply-continuity-ios, a repeated handle and ingest of the same break appends nothing and leaves the decision closed. I placed it beside the existing continuity-break test, not in the pending-reply file, because only that file has the reader harness. - tests/fm-classify-corr-token.test.sh and tests/fm-classify-decision-key.test.sh: the cases that expected a blocked line to append again after a resolve now assert it stays recorded. Verification: - fm-classify-decision-key, fm-classify-corr-token, fm-pending-reply and fm-remote-reply test suites all exit 0 with the fix. - With the two bin files reverted to the commit under review, the new continuity regression and the updated parent-publisher case both fail, so they reproduce ci-1. - shellcheck -x on the changed files reports nothing. - Not finished: a background run of every other suite that mentions the parent channel or pending replies had produced no output, pass or fail, when I returned. One known gap: the escalation writes escalated_epoch before phase. If the process dies between those two writes and an operator resolves the key before the next tick, that tick appends a blocked line for the same episode. I left it, because closing it needs new state. Issue 4755 and other escalation behavior are untouched. Nothing is committed
…ker (kunchenguid#6666) * fix: refuse a confirming Enter on the Claude exit picker The background-task picker still classifies as pending, so a retried Enter confirms "Exit and stop tasks". Stop after the Enter that opened it, and raise the existing stale wake with the dialog name. A non-paused secondmate still skips that wake, so a secondmate parked on the picker still looks idle. Model-downgrade confirmation, MCP approval, and any other Claude exit confirmation are not covered, because there is no recorded screen for them. * no-mistakes(review): fix: anchor exit picker match and wake second mates * fix: refuse a typed submit while the Claude exit picker is open A pane that already shows the picker must not receive the message or a confirming Enter. The match requires the recorded heading and footer lines, and it is skipped when no dialog name is being recorded. * no-mistakes(review): restore watcher to main behaviour, drop dialog wake * no-mistakes(test): test: align Herdr picker fixtures with the preflight read * no-mistakes(document): document exit refusal on a recognised dialog * fix: remove the dialog file when exit runs in a subshell do_exit is invoked from a command substitution, so the parent cleanup never saw the path it is supposed to delete. * no-mistakes(review): fix: set the dialog file path after the control lock * no-mistakes(document): document why the dialog file path follows the lock * no-mistakes(ci): The exit cleanup in bin/fm-control.sh now deletes the dialog file first and releases the control lock second. The changes are in the worktree and are not committed. Invariant: only the process that holds the control lock for a task may write or delete that task's dialog file ($STATE/<id>.composer-dialog, the file that carries the picker name from the composer read to the Enter checks). The old cleanup released the lock and then deleted the file, so the delete ran outside the lock. Places where this invariant must hold: control_cleanup is the only place that deletes the file, and the single assignment after CONTROL_LOCK_HELD=1 is the only place that names it. do_exit only truncates the file, and it runs while the parent holds the lock. A process that loses the lock never sets the path, so it deletes nothing (earlier fix, unchanged). No sibling site needed a change. Fix: in control_cleanup, the rm of the dialog file moved above the fm_lock_release call, with a two-line comment that gives the reason. No dialog recognition or supervision code changed. Test: added test_exit_removes_the_dialog_file_before_releasing_the_lock in tests/fm-control-relaunch.test.sh. It runs one `fm-control exit` with a recording rm on PATH. The lock release removes paths at or under the control lock with rm, so the recording rm writes whether the dialog file exists at that moment. The test asserts the last record is "absent". It starts no second command. Verification, all run locally: - Before the fix, the new test failed with "the dialog file must be gone when the control lock is released, got: present". - After the fix, tests/fm-control-relaunch.test.sh exits 0 with 75 ok lines and no "not ok" line, including the new test and the existing test that the file is gone after exit and after relaunch. - tests/fm-control.test.sh exits 0 with 44 ok lines. - shellcheck -x on both changed files exits 0 with no output. One thing I noticed and did not change: tests/fm-control-relaunch.test.sh prints 615 "rm: cannot remove ... Permission denied" lines during its temp-directory teardown. The count is the same with my change reverted, so this change does not cause it, and the suite still exits 0. I could not read the Greptile check log (the check run was not found), so I worked from the finding text and the user instructions
…henguid#6028) * fix: support stock macOS Bash in timeout watchdog * no-mistakes(ci): Fixed ci-1 in tests/fm-timeout-lib.test.sh: both startup-owner failure paths now kill the bounded command group before killing the watchdog. Fault injection reproduced both leaks before the fix and confirmed cleanup afterward. Bash 3.2 suite, syntax check, ShellCheck, and diff checks passed; GNU timeout coverage skipped because the binary is unavailable. Production code unchanged
…nchenguid#6699) Wrap the cd command in a subshell to comply with the cd-guard policy that blocks persistent top-level directory changes in the primary firstmate checkout. The subshell form (cd projects/<name> && ...) is accepted by the policy as documented in issue kunchenguid#6502. Fixes kunchenguid#6502
* Add daily startup growth check
* no-mistakes(review): watch printed startup memory, retain growth baselines, drop knobs
* no-mistakes(review): delegate budget verdict, report before publish, pin shim home
* no-mistakes(review): baseline first sightings silently, drop mtime, spare secondmates
* no-mistakes(review): cap the wake line, validate budget verdict fields
* no-mistakes(review): guard record schema, check appends, tighten assertions
* no-mistakes(review): drop arbitrary tracked library, make re-arm idempotent
* no-mistakes(review): correct tracked-set wording, restore shim guards, register artifacts
* no-mistakes(review): exit on signal instead of publishing partial record
* no-mistakes(document): correct startup-growth record removal cost in state registry
* no-mistakes(test): sweep orphaned empty startup-growth temp records on due evaluation
* no-mistakes(document): note watcher need for armed startup growth check
* no-mistakes(ci): Diagnosed "Behavior portable serial 5": the shard failed on tests/fm-contributions.test.sh in test_arm_plumbs_a_configured_budget_into_the_check_shim (inherited mode) with "generated check did not attempt a read" and a missing forge/calls file. Root cause: bin/fm-contributions.sh:379-382 computes DEADLINE=$(date +%s)+BUDGET and then gates each read on DEADLINE-$(date +%s) >= OBSERVATION_RESERVE (= min(BUDGET,15)). At the one-second budget this test uses, that gate demands zero elapsed whole seconds, so a wall-clock second boundary crossing between the two date calls makes the poll loop break before any forge read. The test calls wrap_forge (which installs a fake date reading $FORGE/clock only when that file exists) but never seeded forge/clock, so it ran against the real clock. The repo already documents this exact hazard at tests/fm-contributions.test.sh:675-677 and freezes the clock in every other budget-constrained test: test_budget_exhaustion_keeps_prior_record (both modes), test_genuine_failure_near_deadline_is_unavailable, test_reservation_defers_later_url_when_fifteen_seconds_do_not_remain, test_slow_read_deadline_kill_is_budget_refusal, test_budget_is_cut_down_to_the_watcher_check_bound. test_arm_plumbs was the only omission; its single loop body covers both the configured and inherited modes. The remaining wrap_forge users run the default 20-second budget (5 seconds of slack above the reserve) and are not reachable by this race. Fix (smallest, matching the sibling idiom): added the clock freeze `/bin/date +%s > "$home/forge/clock"` plus a two-line comment inside that test's loop, before the poll. Three added lines in tests/fm-contributions.test.sh; no production code and no other file touched. Verification: reproduced the exact CI failure message by running a scratch copy whose fake date advances one second per read (worst case of the unfrozen clock) - missing forge/calls, "not ok - generated check did not attempt a read". With the fix, the single test passes 10/10 standalone and the full tests/fm-contributions.test.sh passes all 46 assertions with exit 0. tests/fm-startup-growth-check.test.sh still exits 0. bash -n and shellcheck -S warning are clean on the edited file; scratch repro files were removed, leaving only the intended three-line change. Note for the author: this flake is not caused by this branch - tests/fm-contributions.test.sh is untouched by the PR and the race is pre-existing, surfacing only on a loaded runner (the shard's neighbouring assertions show 80+ second gaps). It was fixed because it is genuine nondeterminism with an established in-repo remedy and the check cannot otherwise go green. No work was done on the unselected greptile findings (ci-2..ci-5) or the cancelled "Behavior portable serial 2" check (ci-6)
* no-mistakes(ci): Diagnosed "Behavior portable serial 6": the omitted log section (fetched with gh api) shows the shard failed on tests/fm-calm-pi-extension.test.sh in test_hidden_block_geometry_e2e with "Pi Calm hidden-block geometry E2E did not complete the /reload viewport transition" (exit=1, duration_ms=23437; the family line pure-contract-unit count=3 duration_ms=51193 failed=1 matches devin-harness 3203 + calm-pi 23437 + task-delivery 24553). Root cause: tests/fm-calm-pi-extension.test.sh:2831 wait_for_geometry_transition detected the /reload by SAMPLING a single intermediate frame - it polled tmux capture-pane for Pi's transient "Reloading keybindings, extensions, skills, prompts, themes, and context files..." box and only accepted the rebuilt transcript afterwards (elif gated on saw_transient). Pi shows that box only while the reload runs and replaces it via dismissReloadBox when done, so on a loaded runner the box can live entirely between two polls; saw_transient then stays 0 forever and the 600-attempt wait times out although the reload fully succeeded. Reproduced locally under CPU oversubscription with CI's Pi 1.0.4: 1 failure in 8 runs, diagnostics proving the mechanism ("DIAG: transition timeout saw_transient=0 attempt=600") while the captured viewport already showed the completed reload status row and the intact collapsed transcript. Not a Pi-version regression: 1.0.4's handleReloadCommand and both banner strings are identical to the installed 0.85.1, and shard 6 of the previous pipeline run (job 112490351105) passed this same script on Pi 1.0.4. Invariant violated: a TUI E2E assertion must key off a durable state the UI retains, never off one intermediate frame polling may miss. Sibling enumeration: the helper was defined once and called once; grep for transient/saw_/two-stage waits across tests/, bin/ and .pi/ found no other one-shot-frame wait - every other wait in the file keys off durable text presence/absence or off continuously repeating animation frames (the working-ship motion checks) where sampling eventually connects; no doc or other test referenced the helper. Fix (smallest, one helper): renamed it wait_for_geometry_reload and made it wait for the durable status row Pi appends once the reload completed and the chat was rebuilt ("Reloaded keybindings, extensions, skills, prompts, themes, and context files", a persistent chat child via showStatus, present in both 0.85.1 and 1.0.4) together with CALM_GEOMETRY_FINAL. That marker is absent before the reload and never appears if the reload throws, so the check is strictly stronger than before (a failed reload now fails the test instead of passing on transient-then-final). One file changed: tests/fm-calm-pi-extension.test.sh, 2 hunks, no production code. Verification: deterministic before/after - with polls spaced 0.4s apart (the loaded-runner worst case) the pre-fix helper fails with exactly the CI message and the post-fix helper passes; 12/12 consecutive passes of the isolated test under load with Pi 1.0.4; full file 15/15 ok exit 0 under LANG=C.utf8 (matching the runner locale); bash -n, shellcheck -S warning and bin/fm-lint.sh clean; scratch repro files and the temp Pi 1.0.4 install removed, git status shows only the one intended modification. Notes: (1) the flake is not caused by this branch - that test file is untouched by the PR and the race is pre-existing; fixed because it is genuine nondeterminism and the check cannot otherwise go green. (2) Under far harsher load than CI applies a separate unrelated flake appeared in the same test: Pi's "Warning: tmux extended-keys is off..." row occasionally lands between the collapsed [skill] ahoy row and the final response, making assert_geometry_gap see 5 rows instead of 2. Different cause, never seen in CI, and its plausible remedies would change key delivery for the other TUI tests in the file, so it was left alone rather than growing this fix
---------
Co-authored-by: Quartermaster <quartermaster@users.noreply.github.com>
kunchenguid#6708) * fix: reopen a remote-reply continuity break after repair A later break for the same route and reason was swallowed after the operator resolved the first one, because the status line matched for the life of the log. The continuity ingest now appends again when the cursor has moved or retirement has reset that episode, and an unchanged re-read still appends nothing. status_event_recorded is unchanged. Its other callers are the pending-reply escalation, which already decides its own episode, the parent-channel note append, and the remote document transfer note. * no-mistakes(review): seed continuity episode for already recorded break line * no-mistakes(document): document when a remote-reply continuity break reopens * no-mistakes(ci): The adapter now stores the continuity episode record before it appends the `blocked` line, so a failed store appends nothing. The changes are uncommitted in the worktree, in `bin/fm-procevent-remote-reply.sh` and `tests/fm-remote-reply.test.sh`. **Invariant:** a continuity `blocked` line is on the parent status log only when the episode record for that break is already stored. Only the `continuity-broken` branch of `cmd_ingest` appends that line, so the fix is at that one place. **What changed in `cmd_ingest`:** - It decides first whether the break needs a line, then stores the episode record, then appends. - If storing the record fails, it stops with "cannot record continuity episode" and appends nothing. - If the line is already on the log and no episode record exists, it stores the record and does not append. **One addition you did not ask for:** if the append fails after the record is stored, the adapter puts the earlier record back (or deletes the new one when none existed). Without that, the retry would read as the unchanged repeat and append nothing, which is the issue 6701 failure again. **Deviation from your test instruction:** the test does not use `chmod` on the cursor directory. `write_continuity_episode` runs `chmod 700` on that directory before every write, so a read-only directory is made writable again. The test instead makes `mktemp` fail for the episode's temporary file in that directory, the same way the existing receipt-failure test does. **Tests added to `tests/fm-remote-reply.test.sh`:** - Store failure: the second break exits 1, appends no `blocked` line and opens no decision. One retry after storage recovers exits 3 and appends a single line. - Append failure: with the status log read-only, the third break exits 1 and appends nothing. One retry after the log is writable exits 3 and appends a single line. **Verification:** `bash tests/fm-remote-reply.test.sh` ends with "ALL TESTS PASSED" with the fix. Against the script at commit b750c828 the same test file fails at "a continuity break appended its line before its episode was stored". `shellcheck -S warning` reports only an unused loop variable at line 667 of the test file, which this change does not touch. I ran no other test files. The Greptile Review check log could not be retrieved, so I worked from the finding text alone * fix: record a continuity break's reader position on its status line A later break at another cursor is then a different line, so the existing duplicate check appends it and reopens the decision. An unchanged re-read builds the same line and appends nothing. * fix: reopen a continuity break after an identical restore A retirement that puts the same bytes back used to rebuild the recorded line, so the later break stayed closed. The retirement count on that line makes the later break distinct. * no-mistakes(review): remove continuity match for full-prefix line without retirement count * no-mistakes(document): clarify what a continuity break status line records * fix: remove the reply cursor before recording retirement A stop between those steps must leave the count unchanged, so an unchanged continuity break still builds the same line.
…is retired (kunchenguid#6733) * fix(control): drop busy_gen when an incarnation is retired A deliberate exit removed the busy sidecar and left busy_gen in the task record, so the two records disagreed about whether that incarnation was still observable. * no-mistakes(review): drop GNU-only chmod and unreached sidecar-absent branch * no-mistakes(review): correct lock comment to name the deadlock * no-mistakes(ci): The test `test_exit_drops_meta_busy_gen_with_the_sidecar` in tests/fm-control.test.sh now compares the whole task record (the `state/<id>.meta` file), so the Greptile finding is fixed. Invariant: after `exit` retires an incarnation, the task record must equal the record from before `exit` with only the `busy_gen` line removed. This test is the only place in the change that asserts the record survives the rewrite, so it is the only site to fix. The other `busy_gen` tests assert that the line stays, and they do not go through the rewrite. What changed: before `exit`, the test writes the record without its `busy_gen` line to `expected.meta`. After `exit`, the test runs `diff` between that expected copy and the real record, and fails with the diff output if they differ. This one comparison replaces the two earlier checks (no `busy_gen` line left, and the `window` line present), because it covers both. I did not change bin/fm-control.sh or any other file. How I know it works: - I ran `bash tests/fm-control.test.sh`: exit code 0, 45 lines starting with `ok`, no other lines. - I temporarily changed the rewrite in bin/fm-control.sh to also drop the `harness` line. The test then failed with `not ok - exit should drop only busy_gen from the task record:` and the diff `< harness=codex`. The earlier `window`-only check would have passed that rewrite. I restored bin/fm-control.sh afterwards; `git status` shows only tests/fm-control.test.sh modified. - `bash -n` and `shellcheck` on the test file report no new warnings from the edit. The change is not committed; the working tree holds it
…#6484) * test(secondmate-harness): scope fake ps -codex label for pid 5252 to the liveness probe Closes kunchenguid#6456 * no-mistakes(ci): Updated the collision test to log and assert that PID 5252 was queried before selecting 4242. Full fm-secondmate harness suite passes --------- Co-authored-by: YifuGu <ironerumi@users.noreply.github.com>
* fix(calm): share the standalone Pi Calm working-ship widget slot Firstmate Calm and the user-global standalone Pi Calm both install an animated working-ship widget during agent runs. Each claimed its own Pi widget key, so a session loading both (the main Firstmate home) rendered two boats. Pi replaces widgets under one key, so claiming the shared "calm-working-ship" slot keeps dual-install sessions to a single boat while a Firstmate-only session is unchanged. Pins the shared slot contract in the working-ship module test so the key cannot silently diverge again. * test(calm): pin the shared working-ship widget key in CI, document dual-install The key-parity assertion inside the Pi fixture only runs where the @earendil-works/pi-coding-agent package is installed, so CI never exercised it. Add a source-level twin that needs nothing but the tracked file, and note in docs/calm.md that the boat shares the standalone Pi Calm working-row widget slot. * no-mistakes(review): Add executable dual-install widget replacement coverage * no-mistakes(review): Guard shared widget cleanup with disposal ownership * no-mistakes(document): Document shared Calm working-ship slot behavior * test(calm): read the standalone Calm slot from its own module The dual-install check registered both boats itself under the shared slot, so it could only prove that Pi replaces a widget under one key: it would still pass if the standalone Pi Calm extension installed its boat under a different key, which is the two-boat regression the check exists to prevent. Read the standalone extension's own working-ship module when it is installed - FM_STANDALONE_CALM_SHIP, else ~/.pi/agent/extensions/calm - and drive the check with the key that module exports, so a rename on either side registers two widgets and fails naming both keys. A pinned shared-slot contract still covers a machine without the extension, and the run reports which side it used instead of passing silently over an absent extension. Verified: the touched Pi Calm suite passes and reads the installed standalone extension; with a copy of it whose key is renamed to calm-working-ship-v2 the suite fails naming the drift. * no-mistakes(review): Gate stock-row restoration by shared-widget ownership * no-mistakes(review): Removed redundant widget-key source assertions * no-mistakes(document): Document shared Calm working-ship widget ownership
…id#6649) * fix(herdr): make exact-resume presentation-lock wait instead of a bounded timeout The exact-resume path in bin/fm-spawn.sh used the same 50-attempt-then- give-up lock acquire as the new-task-create path, but the two paths are not equivalent on contention: a create has no prior state to strand and can safely fall back to a flat layout, while a resume is recovering a specific existing identity that a concurrent recovery may legitimately be holding the lock for. Giving up there does not degrade gracefully, it hard-fails the resume outright. The suite's own concurrent cross-home recoveries test already asserts both concurrent recoveries succeed with a genuine reclaim, and the file's header comment already (inaccurately) claimed lock contention falls back to the ordinary flat layout for both paths alike, so the intended contract was always that recoveries serialize and both succeed, not that either one refuses under a short bound. Give spawn_herdr_presentation_order_lock_acquire a wait mode that uses this file's own established fm_lock_acquire_wait idiom (already used for its other fleet-shared locks) instead of the bounded loop, and use it only at the exact-resume call site. The new-task-create call site is unchanged and keeps its bounded-then-flat-fallback behavior, which is already covered by its own passing test. Dead-owner PID-liveness reclaim inside fm_lock_try_acquire still bounds the wait against a holder that crashed mid-hold. Adds a deterministic regression test that holds the shared session lock from an unrelated process for well past the old bound, then asserts the resume succeeds with a genuine reclaim and took close to the full hold duration, so a fix that merely widens the bound rather than genuinely waiting is still caught. The existing concurrent cross-home recovery test exercises this under real timing but does not reliably outlast a fixed bound on its own. Corrects the header comment's claim that create and resume share one bounded-then-flat-fallback behavior on lock contention; they no longer do. * no-mistakes(document): Document Herdr recovery waiting for presentation lock * no-mistakes(document): Update stale hard-refusal claim in verification log * no-mistakes(ci): Fixed the Greptile finding on tests/fm-backend-herdr-presentation-e2e.test.sh:1389 by bounding the resume lock-wait regression's spawn_task call. Added an optional 4th `deadline_seconds` arg to the `spawn_task` helper (defaults to empty, so all ~20 other existing call sites are unaffected and unwrapped by `timeout`). The lock-wait test now passes `LOCK_WAIT_HOLD_SECONDS + 60` (90s) as the deadline, and a dedicated check for exit code 124 emits a clear "hung for over Xs instead of waiting out a Ys lock hold" diagnostic before falling through to the existing pass/fail assertions, which are unchanged. No product code was touched. Verified with `bash -n`, `shellcheck -x` (no warnings), a standalone reproduction of the timeout/no-timeout/success paths, the project's `bin/fm-lint.sh --fast` on the file (clean), and the full `tests/fm-lint.test.sh` suite (all 46 assertions pass) * no-mistakes(ci): Replaced the direct `timeout "$deadline_seconds"` call in `spawn_task()` (tests/fm-backend-herdr-presentation-e2e.test.sh) with the repo's portable bounded-execution helper: sourced `bin/fm-timeout-lib.sh` at the top of the file and changed `deadline_cmd=(timeout "$deadline_seconds")` to `deadline_cmd=(fm_run_timed "$deadline_seconds")`. This removes the GNU/BSD `timeout` dependency that would fail with exit 127 on a stock macOS host without coreutils, while preserving identical semantics (exit 124 on bound-hit, command's own exit otherwise), which the existing `[ "$LOCK_WAIT_STATUS" -eq 124 ]` diagnostic check already relies on. Verified: `bash -n` syntax check, `bin/fm-lint.sh --fast` clean, full `tests/fm-lint.test.sh` suite (46/46 pass), and a standalone repro confirming `fm_run_timed` returns 124 on timeout and 0 on success identically to the prior `timeout` call. No other direct `timeout` calls exist in this file or elsewhere in the PR's diff, so no sibling sites remain * fix(herdr): gate exact-resume lock wait behind --herdr-resume-lock-wait Keep refuse-by-default on presentation-order lock contention for Herdr exact resume. Callers that need concurrent recoveries to serialize must pass --herdr-resume-lock-wait; unbounded blocking on a third-party session lock is never the default. Update docs and the real-Herdr e2e suite so the default path asserts the refusal and the opt-in path asserts the wait. * no-mistakes(test): Fix e2e test's lost exit status after if/fi with no else branch * docs(herdr): stop advertising --herdr-resume-lock-wait on --relaunch The relaunch path reuses the recorded endpoint and never takes the presentation-order lock, so the flag is inert there. Drop it from the --relaunch usage line and state where the flag applies. * no-mistakes(review): Clarify lock-wait docs; simplify bash-3.2-safe spawn_task helper * no-mistakes(ci): Fixed ci-1 (Greptile P2). In tests/fm-backend-herdr-presentation-e2e.test.sh, the failure cleanup `cleanup_all` stopped only `LOCK_CONTENTION_OWNER_PID`. It now also stops `LOCK_REFUSE_HOLDER_PID` and `LOCK_WAIT_HOLDER_PID`, the holders of the two new contention cases, so a `fail` before their explicit `wait` no longer leaves them running. Both new PIDs are initialised empty next to the existing one, and each is cleared right after its successful `wait` so cleanup never touches a finished PID. I changed nothing else. `bash -n` passes. The real Herdr e2e run passed both new cases ("default resumed identity refuses session lock contention" and "--herdr-resume-lock-wait waits out session lock contention instead of refusing"). The full run hit my 550s timeout in a later, unrelated case, after the new cases passed
….2 (kunchenguid#6762) * fix(bin): let TERM stop a watcher blocked in a pane capture on bash 3.2 Stock macOS bash 3.2 holds a HUP or TERM until a running command substitution's child exits, and the watcher read every pane through $(fm_backend_capture ...). A blocked backend read therefore held the watcher's stop for as long as the read lasted, and a stopped watcher left the hung read orphaned. tests/fm-watch-triage.test.sh test_term_stops_a_watcher_blocked_inside_a_poll failed on /bin/bash 3.2 for this reason while passing on bash 5. Pane captures now go through watcher_capture, which runs the read as a waited background process group recorded like a check's, so the stop is honored at once and watcher_cleanup stops a read still in flight along with its per-call output file. * no-mistakes(review): Run drain-ring idle capture in watcher shell, add regression test * no-mistakes(document): Document watcher TERM handling for blocked checks and captures * no-mistakes(test): Silence bash 3.2 setpgid race noise from watcher captures * fix(bin): verify the capture group and scope the stop claim to pane reads watcher_capture now confirms its background read leads its own process group, as run_check_capture already does, so watcher_cleanup never relies on a group that set -m failed to create. The comment and continuity doc now say only fm_backend_capture pane reads go through watcher_capture; agent-state and composer-state reads still run inside command substitutions.
* feat(spawn): add per-home worker tool exclusions Add an optional per-home config/crew-exclude-tools file listing tool names to hide from workers, one per line, with blank lines and # comments allowed. It applies to every ship and scout launch and relaunch in that home, is never inherited by another home, and does not affect secondmate agents. Pi and pi-signed apply it through --exclude-tools, which also covers MCP tool names. Any other runtime, and a raw launch command, refuses the launch when the list is non-empty rather than ignoring it. Malformed entries are refused before provisioning, and before a relaunch stops a running worker. Exclusions that match no tool in the worker's loaded registry are reported as unverified warnings in its status record instead of refusing the worker. Closes kunchenguid#6744 * no-mistakes(review): Preserve UTF-8 exclusion paths and verify Pi lifecycle behavior * no-mistakes(document): Clarify worker tool exclusion documentation * no-mistakes(ci): Fixed ci-1 in bin/fm-exclude-tools-lib.sh: a failed read now returns an error before printing names, so all shared launch and relaunch callers refuse rather than silently dropping exclusions. Added deterministic regression coverage for a file disappearing after readability checks across Pi/pi-signed ship and scout launches. Reproduced the original failure; verified 83 spawn checks, 77 relaunch checks, direct parser/runtime failure cases, full targeted lint, Bash syntax, and git diff --check. Relaunch tests passed with existing fixture-cleanup permission warnings. ci-2 remains unchanged per the user's decision; the outer executor owns the fresh CI run
kunchenguid#5343) * refactor(bin): share the local Firstmate home walk from the wake library Teardown's walk over the root home and its registered local secondmate homes moves into bin/fm-wake-lib.sh as fm_local_firstmate_state_dirs, next to fm_firstmate_root_home, so a second consumer can count task records across this machine's homes without a copy. Teardown keeps its exact refusal wording through a thin wrapper. * feat(bin): defer spawns beyond a project's declared machine capacity A project whose machine-local resource only serves a few workers at once had no way to tell Firstmate so: every queued item was launched, and the surplus workers spent full-context turns retrying the resource. config/project-capacity in the root home now declares how many workers each named project admits at once on this machine. bin/fm-spawn.sh counts the ship and scout records on the same project origin across the root and its local secondmate homes, skipping ones whose ready PR is recorded, while holding the shared project lock through publication. A spawn with every place held exits 75 before any brief render, endpoint, worktree, record, or backlog move, so the item stays queued; batches report it as deferred. Undeclared projects keep today's uncapped dispatch, and an unreadable declaration refuses rather than guessing the limit. Refs kunchenguid#4237 * no-mistakes(review): Document that capacity matches the clone directory name * no-mistakes(document): Rewrap stale fm-wake-lib root-home doc comment * no-mistakes(review): Dedupe local state dirs by identity to avoid double-counting * no-mistakes(document): Rewrap fm_local_firstmate_state_dirs error doc comment * no-mistakes(ci): I fixed all four Greptile findings. All 14 tests in tests/fm-project-capacity.test.sh pass, and shellcheck at warning level is clean on the changed files. Each new test failed against the old code and passes now. - **ci-1 (spaced names):** a declaration line must give a name its capacity whenever the name is a valid clone directory name. `fm_project_capacity_lookup` now trims each line, skips blank lines and lines whose first non-blank character is `#`, and takes the last field as the capacity. Everything before that field is the name, so it may contain spaces. The old error cases still refuse: a single field is rejected, and trailing text leaves a last field that is not an integer. The library header and docs/configuration.md now say a name starting with `#` cannot be declared. New test `test_spaced_project_name_is_declared` declares `my heavy project 1` next to an indented comment line and gets a deferral. - **ci-2 (unreadable records):** the holder count must never silently leave out a holder. `fm_project_capacity_occupants` now refuses when a local home's state directory exists but cannot be read or listed, or when a `.meta` file cannot be read. The error names the path, and `fm-spawn.sh` shows it in its existing refusal message. New test `test_unreadable_holders_refuse_admission` covers an unreadable record in the root home and an unreadable state directory in a registered local secondmate home, then checks that the spawn is admitted once both are readable. The test is skipped when run as root. - **ci-3 (Orca lock):** any spawn that can become a holder for a capped project must take that project's lock. The lookup now also reports whether the declaration caps any project at all, and an Orca spawn takes the per-origin lock whenever it does. This covers every capped same-origin clone. It also covers some cases where no same-origin clone is capped, because a spawn cannot find clones under other directory names without searching for them. With no declaration file, Orca still skips the lock. The comments in the library and in the `fm-spawn.sh` header are updated. The Orca test now clones the origin as `project-2`, which has no declaration, and checks that its Orca spawn refuses while the lock is held and publishes no record. - **ci-4 (worktrees):** `assert_nothing_created` now also compares the project's `git worktree list` from before and after a deferred spawn. Both tests that call it take that snapshot first. Files changed: bin/fm-project-capacity-lib.sh, bin/fm-spawn.sh, docs/configuration.md, tests/fm-project-capacity.test.sh * fix(bin): declare capacity for a project name that begins with # A clone directory whose name begins with # was skipped as a comment, so that project stayed uncapped. A line is a declaration when the # is written against the rest of the name and the line ends with a capacity; a # followed by whitespace stays a comment. * no-mistakes(document): Rewrap project-capacity library header comment * no-mistakes(ci): Lint 2 fails because this PR's code pushes ShellCheck past its memory cap. ShellCheck ran out of memory analyzing bin/fm-teardown.sh in CI (reason=memory, rc=251, peak about 8.39 GB). On current main the same file passes at about 7.29 GB. **Cause:** the new `fm_local_firstmate_state_dirs` function in bin/fm-wake-lib.sh had a conditional `. fm-secondmate-registry-lib.sh` with a `# shellcheck source=` directive inside the function. ShellCheck followed that source again, inside a function scope, wherever fm-wake-lib.sh is sourced, and bin/fm-teardown.sh is the heaviest root that sources it. Measured locally with `shellcheck --norc --external-sources bin/fm-teardown.sh`: - current main (fd325b1): 7.29 GB - main merged with this PR: 7.86 GB - the same merge without the in-function source: 7.27 GB **Rule this restores:** this change must not make any lint root heavier than it is on main. That function holds the only new nested source in the change. **Fix:** I removed the in-function source, which no caller needs. Both callers already load the registry library at top level before calling the function: - bin/fm-teardown.sh sources it directly. - bin/fm-spawn.sh, the only user of bin/fm-project-capacity-lib.sh, gets it through bin/fm-ff-lib.sh. I also documented the requirement in the function's comment and in the "Requires" note in bin/fm-project-capacity-lib.sh. No behaviour changes. **Verification:** - ShellCheck on head: bin/fm-teardown.sh peaks at 7.12 GB and bin/fm-spawn.sh at 6.68 GB, both with rc=0. bin/fm-wake-lib.sh and bin/fm-project-capacity-lib.sh lint clean. - tests/fm-project-capacity.test.sh, tests/fm-teardown.test.sh (102 ok) and tests/fm-teardown-endpoint-safety.test.sh all pass. Files changed: bin/fm-wake-lib.sh, bin/fm-project-capacity-lib.sh * fix(bin): release the Herdr session lock when reclaim finishes A concurrent resume in another home waits five seconds for that lock. Reclaim is the last presentation change on the recovery path, so holding the lock through the launch tail made the waiter time out. The contributions arm check also freezes its one-second clock, the same way the budget tests do, because an unfrozen clock can tick past before the first forge read. * no-mistakes(review): Keep Herdr session lock through launch handoff after reclaim * no-mistakes(review): Skip the spawning task's own record in capacity count * no-mistakes(review): Restore release test comment above its test * docs: scope PR-ready re-evaluation to a declared project capacity A ready pull request frees a place only when that project declares capacity, so the always-loaded backlog contract should re-evaluate on that handoff only in that case.
… section (kunchenguid#6785) fm-procevent-lavish.sh read labels tag=message rows SESSION-ENDING MESSAGE only when session_ended is true and CAPTAIN MESSAGE otherwise, but the count line always said session_ending_message_count. Several composer messages on a still-open board were therefore counted as session-ending. The count line now follows the same session_ended switch: session_ending_message_count once the session ended, captain_message_count otherwise. Message rows stay out of the annotation count, per triage. Fixes kunchenguid#6743
…henguid#6780) The Herdr presentation lock namespace was the fixed machine-global /tmp/firstmate-herdr-presentation, so on a host where two OS users run Firstmate on Herdr the first account to create it owned it and every teardown from the other account was refused with no way to clear it. Suffix the namespace with the account uid. The owner-uid and mode-700 checks are unchanged, so a foreign-owned or wrong-mode name at this account's path is still refused and never adopted, chowned, or removed. Fixes kunchenguid#4716.
…te (kunchenguid#6809) The OpenCode session plugin's shouldArm kept its own copy of the need test that only looked for in-flight task records, while the turn-end guard decides with fm_supervision_needed in bin/fm-supervision-lib.sh, which also counts registered process-event sources and trusted custom checks. With an empty fleet but any registered source or check, the guard blocked every turn end while the plugin declined to arm - a loop the guard's own repair line could not resolve because it names the plugin as the fix. The plugin now delegates the decision to the shared predicate through bash, keeping the local away-record decline and the x-mode.env arm override. OpenCode plugin test fixtures now carry the real predicate their arming path sources, and the arm suite gains six cases asserting the plugin's decision against the shared verdict over the same synthetic state directories. Co-authored-by: Mia Sun <mia@Bigs-Mac-mini.localdomain>
kunchenguid#6792) * fix(bin): resolve a pending reply only from its own task's status line Remote reply ingestion handed every corr= token in a mate's payload to fm_pending_reply_try_resolve together with that mate's own status log, so one mate echoing another mate's token resolved the other request. Honor a status-file override only when it is the record's own parent_status, and match the corr= token as a whole word. Fixes kunchenguid#6538 * no-mistakes(document): docs: scope remote reply settlement to the asked mate
…nguid#6784) agent-skill-trigger-index claims to be the complete agent-only trigger index but omitted operational-home-layout, session-start-recovery, validation-supervision, ship-landing, scout-completion, and away-quiet-supervision. Add each with its own description's trigger, placed beside the related entries. The decision-hold-lifecycle redirect stub stays out, per triage. Fixes kunchenguid#6503
…nchenguid#6814) * test: share a rename-safe agent stand-in across liveness suites On Ubuntu 26.04, `sleep` is the uutils multicall binary, which refuses to run when invoked through a symlink named after another utility. The Herdr descendant process-walk tests built their agent-named process as a `pi` symlink to the host `sleep`, so the process exited at once, its parent shell was gone before the walk ran, and both cases read `unknown unreadable` and failed on that host. The suite stops at its first failure, so every later case went unrun. The Herdr control smoke test's `claude` symlink has the same construction. The tmux liveness suite already solved this with a host-compiled spinner and a survival-checked `sleep` fallback. That builder moves into tests/lib.sh as fm_agent_standin, and the tmux suite, both Herdr descendant cases, and the Herdr control smoke test now use it. When no stand-in can survive a foreign name, a case skips with the reason instead of failing. tests/fm-test-fixtures.test.sh gains a portable regression with a fake multicall `sleep`, so it bites on hosts whose own `sleep` is single-purpose. * no-mistakes(document): Correct Herdr verification fixture reference * ci: retrigger cancelled shard
…he Stop hook's group is torn down (kunchenguid#6787) * fix(bin): keep the supervision host's pass-through successor out of the hook's process group The successor a main-only pass-through leaves for main shared the Stop hook's process group, so the harness tearing that group down after the exit-2 rewake stopped it. The stop published downtime and the next park's first cycle announced an empty check: rearm-resurface, which woke main again in a loop. Start that successor in a process group of its own, as the hook's own handling successor already is. * no-mistakes(review): Give the at-turn successor left for main its own group * no-mistakes(document): Document own-group successor for turn-start hand-back too * no-mistakes(ci): I made the change you asked for: both new teardown tests in tests/fm-supervision-host.test.sh now call the existing `stop_home_processes "$home"` just before `pass`. The tests are `test_successor_left_at_the_turn_survives_the_hook_process_group_teardown` and `test_pass_through_successor_survives_the_hook_process_group_teardown`. No production code and no other tests changed. The rule broken was that a test must not leave a home's watcher or arm processes running after it passes. These two were the only cases in the changed area that broke it. The other host+hook tests already stop their home, and `test_successor_close_during_main_turn_is_delivered_at_the_next_turn_end` leaves its watcher behind too, but it is an older test you said not to touch. The only reason anything was left over is that the successor's arm now sits in its own process group, outside the hook's teardown. `stop_home_processes` kills the watcher by the pid in its lock file, which stops it no matter which group it is in. **Checks run:** - I ran just these two tests from a scratch copy of the suite (since deleted). Both pass in about 13 seconds. - After each test, a process listing filtered to that test's home directory came back empty once the processes had about a second to exit after TERM. - `bash -n` on the test file passes. - `shellcheck` is not installed here, so I did not lint the file. - I did not run the full serial-2 suite locally. Whether it now finishes under its 30-minute limit will only show on the next CI run
…ters (kunchenguid#6823) * test: use idle composer readiness for Claude tmux guards * no-mistakes(test): Fix attended supervision test expectations and isolate worker state * no-mistakes(document): Correct live guard coverage and readiness documentation * no-mistakes(ci): Captain, fixed SC2100 by quoting the cursor-agent assignment in tests/fm-host-mirror-live-e2e.test.sh. Reproduced the failure before editing; pinned ShellCheck lint on both PR test files, bash syntax checks, and git diff --check now pass * test: preserve attended successor close assertions * no-mistakes(test): Fix attended live test watcher takeover expectations * no-mistakes(document): Correct stale attended guard documentation * Revert "no-mistakes(document): Correct stale attended guard documentation" This reverts commit 8e59d89. * Revert "no-mistakes(test): Fix attended live test watcher takeover expectations" This reverts commit c0b8510.
* test: order stale watcher-lock fixture races explicitly * no-mistakes(review): Removed duplicate stale-steal reap call
…ting server (kunchenguid#6920) * fix(bin): keep herdr composer probes observable-only without autostarting server - Refuse bare pane identifiers lacking session prefix in fm_backend_herdr_parse_target - Add fm_backend_herdr_target_observable to passively verify server liveness without autostarting - Route visible viewport captures through fm_backend_herdr_target_observable - Detach stdin with </dev/null on background server launch in fm_backend_herdr_server_ensure - Add unit and regression tests in tests/fm-backend-herdr.test.sh * fix(bin): drop parse_target colon restriction to preserve existing fixtures Revert the fm_backend_herdr_parse_target colon check so that single-colon session targets like sess:p1 in secondmate liveness and other fixtures parse properly. The probe path remains observable-only via fm_backend_herdr_target_observable.
…kunchenguid#6956) * test: clear inherited Git repository location in fixture helper An absolute GIT_DIR inherited from the caller (a linked-worktree hook exports one) overrides the discovery that git -C implies, so fixture setup moved the caller's branches and registered fixture worktrees in it. tests/git-config-helpers.sh now unsets GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE and GIT_COMMON_DIR alongside the config isolation. Fixes kunchenguid#6953 * no-mistakes(document): Document fixture Git location isolation in CONTRIBUTING
…nchenguid#6830) * fix(bin): stage contribution-input JSON via temp files The contribution-input branch passed the whole canonical backlog document to jq as a single --argjson argv argument. A large fleet's backlog JSON exceeds Linux MAX_ARG_STRLEN (128 KiB), so jq fails with E2BIG (Argument list too long) and the contribution-input snapshot emits empty output, breaking the bearings contribution path. Write both the backlog and task documents to a temp directory and hand them to jq with --slurpfile, matching how the full snapshot path already stages the same pair. Output is unchanged for any backlog that fits in argv. * no-mistakes(document): Correct contribution cleanup trap comment
) * fix(bin): arm the busy contract on secondmate launches A tmux-backed secondmate's active-turn gate can only see a busy verdict from the parent home's record, and those launches never armed it. * no-mistakes(review): Reopen secondmate busy on blocked Claude Stop; drop muse guard * no-mistakes(review): Make Claude secondmate Stop guard the sole Stop-state writer * no-mistakes(review): Record live rewake UserPromptSubmit evidence and add regression * docs: record Claude secondmate Stop guard as busy writer * no-mistakes(review): Keep Stop idle hook for older secondmate guards * no-mistakes(document): Correct stale omp secondmate -e rationale in supervision verification * no-mistakes(document): Fix skill-relative turnend-guard link in Claude harness reference * no-mistakes(ci): Root cause: in shard 9, the pure-contract-unit family ran two scripts, tests/fm-rovo-harness.test.sh (14712 ms) and tests/fm-omp-harness.test.sh (10335 ms). Those two durations add up exactly to the failing family's 25047 ms. The failing one was tests/fm-omp-harness.test.sh. Its test_secondmate_launch_relies_on_discovery still asserted the old contract: that an omp secondmate launch names no -e and gets no state/<id>.omp-ext.ts. This PR (issue kunchenguid#4268) deliberately changed that. bin/fm-spawn.sh now arms the busy contract for omp secondmates, so their launch names `-e state/<id>.omp-ext.ts` and writes that extension. The primary extensions are still left to auto-discovery. The harness reference docs were updated to match, but the test was not. The fix is test-only and touches just tests/fm-omp-harness.test.sh. The test now asserts that the secondmate launch names exactly one -e and that it points at the parent-home state/sm.omp-ext.ts. Because only one -e is allowed, the tracked .omp/extensions files are not named a second time, which would load them twice. The test also asserts that the extension file exists, and the header comment for contract 3 was updated. The posture overlay, --cwd, marker, and supervision-model assertions are unchanged. Verification: the host's load average was about 140 on 8 cores, and the full script timed out locally after 590 s having finished only its first 2 tests. So I ran the one affected test function on its own through the real fm-spawn path, and it passed (`ok - fm-spawn: a real omp secondmate launch loads only the busy -e and auto-discovers its primary extensions`). The remaining omp tests were not re-run locally. shellcheck on the file reports only existing info-level notices. The change is left uncommitted for the outer executor * test: keep the secondmate busy stall gate visible after main's liveness probe The merged watcher treats a window-name inventory miss as a missing endpoint and exits a later checkpoint on the previous quiet run's recovery wake. The fake pane now answers both list-windows formats, and each observation starts without a leftover downtime marker, so a busy mate still defers the stall and an idle mate still surfaces it. * no-mistakes(review): Align fm-spawn secondmate comments with omp -e and devin refusal * no-mistakes(document): Document secondmate busy extensions in configuration reference * no-mistakes(test): Make live busy-contract guard clean up read-only lab dirs * fix(bin): pass Claude secondmate busy hooks through --settings Rewriting a persistent mate home's settings.local.json drops the captain's saved permissions. The hooks now live in a parent-state file Claude Code loads for that session only. * no-mistakes(review): skip busy arm for raw secondmate launch commands * no-mistakes(review): retire earlier busy arm on raw secondmate respawn * no-mistakes(review): retire Stop pointer on relaunch, load --settings in live guard * no-mistakes(document): Document secondmate Claude settings file and raw-launch exception * no-mistakes(ci): The check "Behavior portable serial 9" failed because of this PR, and I fixed it with a test-only change; `tests/fm-spawn-dispatch-profile.test.sh` now passes locally in full. Cause: the one failing test was "claude secondmate launch settings JSON does not disable Claude attribution" in `tests/fm-spawn-dispatch-profile.test.sh`. This PR makes a claude secondmate launch pass `--settings` as a file path (`state/<id>.claude-settings.json`) instead of inline JSON. The test helper `claude_settings_json_arg` handed that path string to jq as if it were JSON, so jq reported a parse error. The product behavior is correct: the file does carry `feedbackDrafts` and the attribution-off policy. Invariant: a test that checks the Claude launch settings must read the settings Claude would load, whichever form `--settings` takes (inline JSON or a file path). The only place this must hold is `claude_settings_json_arg`, the single reader of the `--settings` value in the tests. Its two callers, `assert_attribution_policy` and `assert_attribution_policy_absent`, both go through it. Fix: `claude_settings_json_arg` now prints the value when it starts with `{` and prints the file contents otherwise. No product code changed. Verification: I ran `bash tests/fm-spawn-dispatch-profile.test.sh` in the worktree. It ended with "all fm-spawn-dispatch-profile tests passed", including the previously failing secondmate case and the crewmate and keep-ai-trailers cases that use the inline form. I did not run the other tests of shard 9; they passed in the CI run and this change touches only this test file. The change is uncommitted in the worktree (one file modified)
…ailures (kunchenguid#7036) The large-backlog regression already proved the json and secondmate home-summary modes stay within exec limits. It now also runs the contribution-input mode on the same 1200-record backlog, which is the path kunchenguid#6830 moved off argv. With that fix reverted the test fails with the field error jq Argument list too long at the contribution-input call.
…urn (kunchenguid#5843) * fix(teardown): scrub nested repos before return Treehouse return uses git clean -fd, which leaves a nested git repository in the pool slot. Scout and forced teardown run git clean -ffdq first. A return that stays dirty names the lost slot instead of reporting a clean return. * no-mistakes(review): Report lost pool slots early and scrub forced child copies * no-mistakes(review): Drop misleading no-rerun clause from lost-slot message * no-mistakes(document): Note failed status read in teardown dirty-return header * no-mistakes(ci): I fixed ci-1 and ci-2 in bin/fm-teardown.sh, and I added tests in tests/fm-teardown.test.sh. Invariant (ci-1): a lost-slot finding must only warn. It must never change the teardown exit status. Two places must obey this: the final status block, and the remote success path that a forced remote retirement uses. - The final block no longer runs `exit 1`. When a slot is lost, teardown writes a lost-slot line to stderr instead of the clean "teardown X complete" line. Then backlog_refresh_reminder runs and teardown exits with its normal status. - Before this change, remote_secondmate_teardown discarded the remote output when the remote run succeeded. Now it sends that output to stderr. Without this, the remote lost-slot warning would not reach the operator once the remote side exits 0. - I updated the header comment to match. Invariant (ci-2): after a return that succeeds, teardown must verify every returned copy, or it must warn that it could not. This invariant is enforced inside teardown_note_dirty_return, which both return sites call (the main WT return and the forced-secondmate child return). Before, a copy that was missing or not a readable git worktree caused a silent `return 0`. Now it sets the lost flag and gives the same warning, with the text "cannot verify: the copy is missing or not a readable git worktree after return". Both call sites run only for a copy that existed before the return. The exit status does not change. Tests: - The dirty-return test now expects exit 0 and the warning. - New test (z3d): a forced secondmate teardown whose child return leaves residue. This is the same command that `retire --force` runs on the remote host. The test expects exit 0 and the warning, and it checks that the task record and the home are removed. - New test (z3e): a return that removes the copy's .git. The test expects exit 0, the "cannot verify" warning that names the copy, and no clean completion line. Verification: - I ran the new and updated tests against the old script. Three failed as expected (exit 1, or no warning). With the fix, all of them pass. - The full tests/fm-teardown.test.sh suite passes: 117 ok, 0 not ok. - shellcheck shows no new warnings. - tests/fm-remote-secondmate-lifecycle-e2e.test.sh is still running. It covers the remote success path that now relays output. Up to now it has no failures, but it has not finished, so I cannot report a final result for it * no-mistakes(ci): I fixed ci-1 and ci-2 as one exit contract, as the user asked. The changes are in bin/fm-teardown.sh, tests/fm-teardown.test.sh, and tests/fm-remote-secondmate-lifecycle-e2e.test.sh. Invariant (ci-1): when a returned slot is dirty or cannot be verified, teardown must finish every cleanup step and then exit with exit code 4. Exit 4 is used for nothing else. It must hold in two places: the final status of a local teardown, and the remote secondmate retirement caller. - Local: the existing lost-slot flag now leads to `exit 4` (TEARDOWN_LOST_SLOT_EXIT). This exit comes after the task record is removed, the warning is printed, and backlog_refresh_reminder runs. A clean return still exits 0. Refusals and errors exit 1, and an invalid request exits 2. The header documents all of these codes. Exit 3 is only an internal "not remote" return. The ssh transport uses 255. - Remote caller: remote_secondmate_teardown treats a remote exit 4 as "retired with a lost-slot warning". It does the full local retirement (route, registry line, reply source, records, summary refresh), prints the normal completion line on stdout, and then returns 4. The remote control script runs fm-teardown.sh as its last command, and fm-on.sh execs ssh, so the remote 4 reaches the local side unchanged. Invariant (ci-2): remote output goes to local stderr only when it is a lost-slot warning. - After a clean remote retirement (exit 0), the caller no longer prints the captured remote output. - After exit 4, the caller forwards only the lost-slot warning header and the remaining-path lines. To make those lines easy to find, teardown_note_dirty_return now indents each remaining path by two spaces. Tests: - The dirty-return (z3), forced-secondmate dirty-child (z3d), and copy-removed (z3e) tests now expect exit 4 after cleanup. The clean scout return (z1) still expects 0. - The final remote retirement in the e2e test now uses a fake-ssh mode, retire-lost-slot. In this mode the real remote retire runs, and then the fake reports a lost child slot with exit 4. The test checks the following: exit 4; the remote home, parent metadata, and route are removed; stdout has "teardown ios complete (remote"; stderr has the warning and the remaining path; and stderr does not contain the remote completion text. The remote side producing exit 4 is covered by z3d. Verification: - tests/fm-teardown.test.sh: 117 ok, 0 not ok. - tests/fm-remote-secondmate-lifecycle-e2e.test.sh: ALL TESTS PASSED. - shellcheck shows the same warning count as before on all three files. - Against the previous script, both suites fail as expected. In the local suite, dirty-return expected 4 but got 0. In the e2e test, the stdout local-completion check fails * no-mistakes(ci): Both CI failures came from the lost-slot check that this PR added. Shard 3: tests/fm-secondmate-safety.test.sh "force teardown failed to discard child work". Shard 7: tests/fm-teardown-endpoint-safety.test.sh exited 4. The check ran after the return of every copy. Treehouse return removes a copy that is not a pool slot, so the check reported "cannot verify" and teardown exited 4. Before the fix, both failures happened on my machine. Invariant: only a Treehouse pool slot can be lost by the pool, so the check runs only for a copy that was a pool slot before the return. Fix in bin/fm-teardown.sh: both return sites (the main WT return and the forced-secondmate child return) now run fm_treehouse_pool_slot before the return. They call teardown_note_dirty_return only when the copy was a pool slot. teardown_note_dirty_return lost its pool-slot branch and its unused proj argument; the slot label still comes from the path of the copy. Test fix: in tests/fm-teardown.test.sh, the z3d child copy was not a pool slot. It is now a real pool slot (pool/6/repo plus treehouse-state.json), and the test still expects exit 4 and the warning. Verification: endpoint-safety 27 ok, exit 0. secondmate-safety 79 ok, exit 0. fm-teardown 117 ok, 0 not ok. remote-secondmate-lifecycle-e2e ALL TESTS PASSED. shellcheck count is unchanged (17) --------- Co-authored-by: Nova Agent B <novaagentb@gmail.com>
…nguid#7047) * fix(tests): keep backlog suites hermetic against a local .tasks.toml Fixture homes now write the tracked markdown defaults through a fm_test_tasks_toml helper instead of copying the checkout's .tasks.toml, which an operator may override with a beads backend at a live backlog. The backlog handoff and receive moves pin --backend markdown, because they already name their markdown files and otherwise resolve the backend from the caller's working directory. The atomicity suite runs from its scratch root so its direct tasks-axi fixture calls cannot read the checkout config. * no-mistakes(review): Add decoy-cwd .tasks.toml regressions for handoff and receive * no-mistakes(lint): Reuse sha256_file helper to fix SC2015 in remote handoff test
…nchenguid#7076) * fix(bin): parse whole project name in home seed registry queries Parse the full project name after the dash in fm-home-seed.sh registry lookups. This matches the round-3 bootstrap mirror check parser so spaced names resolve. Cover both the project-less seed refusal guard and registry line lookups in tests. * no-mistakes(review): Match registry rows by name prefix plus separator * no-mistakes(document): Note shared registry row-match rule in fm-home-seed
…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.
# Conflicts: # bin/fm-secondmate-liveness-lib.sh # bin/fm-timeout-lib.sh # bin/fm-wake-lib.sh # docs/architecture.md # docs/configuration.md # tests/fm-timeout-lib.test.sh
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.
Summary
Merges
upstream/main(kunchenguid/firstmate) into the fork'smainwith a merge commit.Upstream range merged:
e31bc6e6..2911a3bb(49 commits, previous sync pointe31bc6e6).No rebase of fork history, no force-push, no push to
upstream.Conflicts and resolutions
Five files conflicted; all resolved by hand, keeping both sides' behavior except where noted below.
bin/fm-timeout-lib.sh- the fork'sself=${BASHPID:-...}owner-resolution workaround is superseded by upstream's own fix (see below): took upstream's side, which removes the need for a BASHPID read entirely by passing the pre-exec$PPIDinto the perl watchdog instead.tests/fm-timeout-lib.test.sh- two spots read$BASHPIDwith a fork-added portable fallback; took upstream's replacement, which readsperl -e 'print getppid()'instead of touching$BASHPIDat all, for the same reason.bin/fm-secondmate-liveness-lib.sh- both sides independently touchedfm_secondmate_liveness_relaunch's local-variable line (fork addedspawn_lockfor its own per-task spawn-lock probe, upstream addedrelaunch_lock/wait_seconds/rcfor its new per-home relaunch lock). The function bodies themselves auto-merged cleanly and the two mechanisms compose (spawn-lock probe runs first, then the per-home relaunch lock is acquired), so the resolution combines both sets of locals on onelocalline. The header comment block describing the locking contract was merged to describe both the fork's startup-grace/spawn-lock-skip behavior and upstream's per-home relaunch-lock behavior.bin/fm-wake-lib.sh- both sides added a new function in the same location (fork:fm_treehouse_home_pool_root; upstream:fm_local_firstmate_state_dirs, used by upstream's new per-project capacity admission). Kept both functions, and merged the two differing first lines of the following "one lock serializing Treehouse slot allocation..." doc comment into upstream's fuller version (it also now covers project-capacity admission), which already flowed correctly into the unchanged fork-authored paragraph below it describingfm_treehouse_home_pool_root's effect on that lock's identity.docs/architecture.md,docs/configuration.md- doc-only conflicts describing the samefm-secondmate-liveness-lib.shbehavior from each side; merged to document both the fork's startup-grace/spawn-lock contract and upstream's per-home relaunch-lock contract, including updatingFM_SECONDMATE_LIVENESS_TIMEOUT's description to upstream's more accurate one (it now separately bounds the per-home lock wait and the spawn execution, confirmed against the actual code).Fork change status
--modelcredentialed-match guard (#4)5838f105(fix: restore timeout watchdog compatibility with macOS Bash 3.2, kunchenguid#6028) - upstream solved the same BASHPID-unbound problem by eliminating the BASHPID read altogether (passes$PPIDinto the perl watchdog and lets it compare against its own$$, whichexecpreserves as the shell's pid), rather than working around the missing variable. Fork's workaround was dropped in favor of upstream's cleaner fix.fm_sm_live_within_startup_grace) and per-task spawn-lock probe infm_secondmate_liveness_relaunchaddress a different race (a fresh/concurrent manual spawn) than upstream6407052f(fix: prevent concurrent secondmate relaunch races, kunchenguid#7154), which adds a per-home lock serializing relaunches of different mates against each other. Neither supersedes the other; both are kept and now compose in the same function.fede6197(test(pi): accept Pi 1.0.1's renamed HTML export renderer lookup, kunchenguid#6530) targets exactly this failure, but this host has no npm-installed@earendil-works/pi-coding-agentpackage (only a Homebrew-installedpi1.0.4 binary), so every assertion intests/fm-calm-pi-extension.test.shthat depends on the installed package capability-skipped here ("skip: installed @earendil-works/pi-coding-agent package not found"). I cannot confirm from this host whether upstream's fix makes the test pass under Pi 1.0.1; that needs a run where the npm package at 1.0.1 is actually installed (e.g. CI with the pin temporarily bumped, out of scope here).Upstream commits also noted in the captain's brief as already covered by this merge:
5838f105(bash 3.2 timeout watchdog, superseding fork commit above),fede6197(Pi 1.0.1 Calm test renderer lookup, unverified here as above),53b5bc11(Pi Calm working-ship widget slot, kunchenguid#1854),8c9c67de(Pibefore_agent_startnarrowing, kunchenguid#7248) - all now present on this branch as part of the merged range, no further action needed for them.Bash 3.2 / BASHPID audit
Grepped the merged
bin/forBASHPIDand other bash-4-only constructs reachable at runtime on stock macOS/bin/bash3.2 (confirmed3.2.57(1)-releaseon this host):$BASHPIDreference uses the${BASHPID:-$$}(or equivalent) default-expansion fallback, which is bash-3.2-safe even underset -u; none read$BASHPIDunguarded.declare -A/typeset -A,mapfile/readarray(the onemapfilehit infm-procevent-remote-reply.shis anawkvariable name, not the bash builtin),${var,,}/${var^^}case conversion,wait -n,coproc,declare -n/local -nnamerefs, negative array indices,globstar, and${parameter@op}transformations: none found inbin/.printf '%(...)T'(bash 4.2+) infm-wake-lib.shis version-gated behind aBASH_VERSINFOcheck with adate-forking fallback for bash < 4.2.$EPOCHREALTIMEinfm-lint.shandfm-timing-lib.shis read through${EPOCHREALTIME:-}, which is empty (not unbound) under bash 3.2 and both callers fall back correctly.No unguarded bash-4-only construct found reachable at runtime.
Test results
Run under the host's stock
bash(3.2.57), perfirstmate-coding-guidelines:tests/fm-timeout-lib.test.sh- all passtests/fm-pi-model-guard.test.sh- all passtests/fm-secondmate-liveness.test.sh- all pass (covers both the fork's startup-grace/spawn-lock-skip tests and upstream's new per-home relaunch-lock-wait test)tests/fm-treehouse-home-pool-root.test.sh- all passtests/fm-project-capacity.test.sh- all passtests/fm-spawn-pool-base-freshen.test.sh- all passtests/fm-teardown-endpoint-safety.test.sh- all pass (tmux-dependent cases capability-skip; tmux not installed on this host)tests/fm-spawn-dispatch-profile.test.sh- all pass (long-running, ~8 min)tests/fm-calm-pi-extension.test.sh- passed on 3 of 4 runs; one run hitnot ok - render_export_dom did not exhaust its bounded retries on a Chrome that never finished, not reproduced on reruns (transient/timing, not a merge regression). Every case gated on an installed@earendil-works/pi-coding-agentnpm package capability-skipped (none installed; see Pi 1.0.1 note above).bin/fm-lint.sh(ShellCheck + workflow lint) - cleanbin/fm-doc-audience-check.sh- clean (119 surfaces, 758 local links)Not run: the full repo-wide suite (out of scope; targeted validation per
firstmate-coding-guidelines' no-mistakes test-configuration note, and this is a direct-PR task, not a no-mistakes run).Out of scope
No new feature or fix beyond the merge itself. The CI Pi version pin is unchanged. Updating any running homes onto this merge is firstmate's job after one real spawn on this Mac, not part of this PR.