Repository navigation
feat(bin): merge upstream kunchenguid/firstmate main into the fork - #52
Merged
Merged
Conversation
…id#4854) Captain holds have no due semantics and are a hold kind, not a Beads issue type. The create path now waives due.required and maps to native type task. Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(bin): launch every spawned agent with the compact adviser disabled Every crewmate, scout, and secondmate Firstmate launches now starts with COMPACT_ADVISER_DISABLE=1, on a fresh spawn and on a relaunch alike, so an unattended session never activates the compact adviser. The value is unconditional: no configuration file gates it and there is no override, unlike the trace carrier beside it. Three carriers deliver it, because no single one covers every launch shape. The pane shell receives an export beside GOTMPDIR, so the agent's own children inherit it too. The launch command carries an explicit assignment, prepended outermost so it wins over any ambient value the pane already held. The cleared launch environment sets it again at the `env -i` boundary and keeps COMPACT_ADVISER_DISABLE in the fixed operational floor, which is what preserves the switch when config/launch-env-allowlist empties the environment, and what delivers it on a remote host that never had the value. bin/fm-control.sh relaunch, the bootstrap secondmate relaunch, and the remote secondmate transport all rebuild their launch through bin/fm-spawn.sh, so they inherit the same floor. The captain's own primary session is untouched. The two new suites drive the real spawn and then execute the launch command the pane actually received, with the harness replaced by a probe that prints its own environment, rather than matching script text. They cover ship and secondmate launches with the allowlist absent and enabled, the pane export and its ordering, fm-control.sh relaunch, and the full parent to remote-host chain. * no-mistakes(review): Export compact-adviser disable across compound launches * no-mistakes(document): Document spawned-agent compact-adviser environment guarantee
…henguid#4894) * fix(bin): let a background Claude session keep owning its session lock Session-lock ownership was decided by process ancestry alone. Under an unattended Claude session the model loop runs in a transient bg-spare bridged to the front-end by a shared daemon; when that bridge is recycled the contiguous claude-named ancestry from a hook to the recorded owner breaks while the owner pid stays alive, so the Stop auto-arm stood down as a foreign live owner, the turn-end guard ended every turn with its read-only diagnostic, and fm-lock.sh refused - a self-sustaining outage until restart. Ownership is now ancestry membership OR a trusted same-session id, never id-first: - fm-session-lock-lib.sh accepts CLAUDE_CODE_SESSION_ID only when CLAUDE_PID is a Claude-shaped member of the current contiguous run, compares it against the id recorded in state/.lock-session, and requires the recorded pid to still be a live harness. No id, no sidecar, an untrusted id, a different id, or a dead recorded pid leaves the ancestry verdict unchanged. Ids are never read from ps argv. - fm-lock.sh accepts a same-session holder at both refusal sites, writes, refreshes, and clears the sidecar only under its claim lock (including the early already-mine exit, skipped only while the deferred startup sweep leases that lock), keeps it byte-identical across a same-session confirmation, records CLAUDE_PID on lock line 1 for a session with a trusted id so a shared daemon or front-end that outlives the session never keeps a dead session's lock alive, never rewrites a live line 1 on a same-session confirmation, and names the recorded id in the live-owner refusal. - The .lock line-1 format is unchanged, so every reader that takes the whole first line as the pid keeps working; the guard's foreign-owner exit is unchanged and inherits the fix through the shared predicate. Tests: the ancestry suite drives the ancestry and id signals apart in a deterministic process table (asserting the divergence) and runs a real orphaned front-end/daemon/pty-host/spare tree through six phases with the real lock, auto-arm, and guard scripts; the foreign-owner repro keeps its negative control and adds a same-id positive control. Disclosure: no live unattended Claude background session ran on the verifying machine. The topology is documented by the real process listings in kunchenguid#3902, kunchenguid#2314, kunchenguid#3398, and kunchenguid#4066; coverage is the structural predicate plus the executable fixtures, not a live pass. Residual: bin/fm-sessionstart-nudge.sh keeps its own private ancestry walk (it only decides whether to print a nudge) and may nudge on a resume in the recycled case. Out of scope, deliberately: no structured lock format, no guard budget changes, no daemon-identity rejection, no fork lineage. * no-mistakes(review): Wait for claim lock; revert failed sidecars * no-mistakes(review): Revalidate ownership after wait; restore sidecars * no-mistakes(review): Roll back sidecar by publication phase * no-mistakes(review): Restore sidecar only if lock line is unchanged * no-mistakes(review): Trust session ids without a spelling allowlist * no-mistakes(review): Disarm sidecar rollback before backup cleanup * no-mistakes(document): Updated session-lock ownership documentation
* feat: park main under the away posture on Pi While the away-posture record exists on a Pi primary, the supervision branch takes every actionable wake, no processing turn opens on main, captain rows accumulate for the return brief, and main's standing authority relocates to the branch through the existing guarded scripts. - lib/fm-branch-dispatch.ts: read the record at every routing decision; while it exists claim check, decision-owned, and heartbeat rows too, keeping the two broken-queue vetoes; expose checkSeqs so a claimed check row lifts task scoping. - fm-primary-pi-watch.ts: offer every actionable row under the record; a declined wake and every watcher-failure alarm still reach main. - fm-branch-supervision.ts: drop the legacy .afk decline; append a fixed POSTURE: AWAY tail carrying the record's read-back verbatim per wake; open no processing request while the record exists, re-checked immediately before a request would open and at every run boundary; present the accumulated rows at the first run boundary after archive. - fm-lease-lib.sh: fm_lease_forbid_branch passes the branch for opted-in actions only while fm-afk-contract.sh validate succeeds on a confirmed live record; PR merge, fresh spawn, and decision answer opt in, local landing never does. - fm-send.sh: a --resolve-key naming an open needs-decision or captain-held task is a decision answer and meets the partition; blocked: keys stay steering. - fm-spawn.sh: enforce the record's spend cap for a fresh ordinary spawn by either actor; relaunches and secondmates exempt. - fm-branch-prompt.sh: fixed Postures section and the verbatim ask-user-authority policy; the prefix stays byte-stable. - fm-afk-return.sh: count what the away session handled from the store. - docs, afk skill, AGENTS.md stub: main parked on Pi, green merge gate absolute while away. - tests: watcher and branch extension suites, fleet-record, merge, and decision-answer suites cover the relocation, the vetoes, the tail, the parked processing turn, the cancellation, the re-presentation, and the spend cap; dated live-guard evidence recorded. * no-mistakes(review): Refuse branch merge after preflight archive race * no-mistakes(review): Fix away wake, spawn, and processing races * no-mistakes(review): Suppress parked processing; narrow away-only rejection * no-mistakes(review): Abort dedicated processing; gate branch spawn once * no-mistakes(review): Stamp away-only on the dispatch offer * no-mistakes(review): Treat invalid away records as spend-cap absence * no-mistakes(review): Drop spawn test hook; abort processing-opened runs * no-mistakes(review): Bind abort to opening prompt; cap-read absence * no-mistakes(review): Limit away branch spawn to queued work only * no-mistakes(document): Correct AFK posture documentation
* ci: simplify CI job timeouts to a three-tier policy Replace the scattered per-job timeout values (10m parallel, 25m lint, 30m serial, 10m macOS) with three readable tiers, each a hang tripwire with headroom rather than a packing estimate: - fast (5m): coverage guard, repo invariants, timing aggregate - normal (30m, one shared budget): lint partitions, portable parallel shards, portable serial shards, macOS stock Bash - heavy (Herdr only): 20m step tripwire on the family run so always() cleanup still runs, under a 75m job-level last-resort backstop The workflow's header comment states the policy and points at docs/fm-test-portable-shards.md "Timeouts", which now owns it, and each job names its tier beside timeout-minutes. tests/fm-ci-workflow.test.sh asserts the policy against the parsed workflow instead of the old per-job minute values: every job joins exactly one tier, exactly three distinct job-level values exist, the fast tier stays within 5-10 minutes, the normal budget stays at least double the modeled parallel lane sum reported by fm-test-run.sh --check-coverage, and the Herdr step tripwire stays below its job backstop with an always() cleanup after it. Concurrency supersession, shard counts, lane membership, and fail-fast settings are unchanged. * no-mistakes(review): Decouple the normal timeout from packing estimates * no-mistakes(review): Assert Herdr teardown follows the family run * no-mistakes(review): Pin Herdr family-run timeout to 20 minutes * no-mistakes(review): Ignore comments when identifying Herdr steps * no-mistakes(review): Identify Herdr steps by declarative ids * no-mistakes(document): Clarify authoritative three-tier timeout policy
…nchenguid#4895) * fix(bin): keep supervisor status closes from waking the same home A drain that already folded OPEN DECISIONS has presented those bytes even when the watcher has no matching seen marker. Treat that fold, and the presentation cursor, as known so the bookkeeping close stays quiet while later worker lines still signal. * no-mistakes(review): Keep folded worker failures waking past supervisor closes * no-mistakes(review): Wake on unlisted folded worker lines; batch multi-key closes * no-mistakes(review): Stop folded worker resolved lines from counting as already read * no-mistakes(document): Correct self-announced close marker contract in docs
* Stop steering operators away from Herdr * no-mistakes(review): Neutralize remaining Herdr opt-out documentation wording
…enguid#4973) * fix(bin): treat a live no-mistakes run as current after rebase A running run on the task's branch is authoritative regardless of head. Matching only the local head made a rebased in-flight run look failed. * no-mistakes(review): restrict coarse live-any-head to foreign-branch answers * no-mistakes(review): reject gate-parked runs from the executing predicate * no-mistakes(review): hoist gate-marker patterns into single run-lib owner * no-mistakes(review): require live daemon for head-free run binding * no-mistakes(review): require answered daemon-down before unbinding live runs * no-mistakes(review): extend daemon guard to anchored continuation routes * no-mistakes(review): delete live-any-head; restore dead-daemon verdict * no-mistakes(review): keep parked gates parked; name dead daemon everywhere * no-mistakes(review): set dead-daemon verdict instead of emitting early * no-mistakes(review): align selected route with legacy dead-daemon handling * no-mistakes(review): drop unproven-record binds; narrow coarse gate reading * no-mistakes(review): narrow header, drop vestigial guard, retarget tests * no-mistakes(review): revert coarse gate override; require answered-down probe * no-mistakes(review): cache one daemon probe; stop duplicating run id * no-mistakes(review): restrict coarse dead-daemon verdict to moved-off rows * no-mistakes(review): delete coarse dead-daemon extension and gate note * no-mistakes(review): delete remaining coarse dead-daemon block and stale docs * no-mistakes(document): document rebase-safe live-run bind and unverified-record verdict
…4994) * fix(bin): stage the launch command in a private file and type a short source line A long launch line typed while the fresh pane shell is still busy waits in the terminal's canonical line buffer, which drops input past about 1,024 bytes on macOS, so the pane was left at an unfinished command with no agent running. fm-spawn now writes the assembled command to the task's own temp root under umask 077 and types only a short line that sources it. Refs kunchenguid#4559 * fix(bin): keep the per-task temp root private before staging the launch command The root lives at a predictable path under /tmp and now holds the whole launch command. Create it with mode 0700, refuse one that already exists as anything but a directory owned by this user that nobody else can write, and tighten an owned one, so no other local user can plant or swap the staged file. Refs kunchenguid#4559 * fix(bin): enforce private staged launch file mode * test(spawn): cover long staged Claude launches * no-mistakes(review): Namespace launch files and prove truncation staging * no-mistakes(review): Use immutable per-spawn launch filenames * no-mistakes(document): Document staged launch delivery safeguards * no-mistakes(ci): Updated eight behavior tests/fakes to execute or inspect immutable staged launch files instead of expecting inline launch commands. This restores Muse, secondmate lifecycle/restart, remote trace/parent binding, compact-adviser, and Orca coverage. All affected tests, dispatch-profile regression, fixture tests, syntax checks, ShellCheck, and git diff checks pass --------- Co-authored-by: Vytautas Stankus <svycka@gmail.com>
* Add isolated Herdr runbook to test instructions * no-mistakes(review): Drop substring matching from test.instructions contract * no-mistakes(review): Assert commands.test key absence in YAML * Drop unit-first sentence and instructions contract test Captain-scoped follow-up on the Herdr-lab test.instructions ship: keep the lab safety runbook only, and leave the no-mistakes contract test focused on commands.test absence.
…uid#4873) (kunchenguid#5001) * docs(vision): accept vendor-semantics and 9k contract-ceiling amendments (kunchenguid#4873) Replace the pixels-of-today's-UI rule with a quarantined, version-pinned surface-adapter exception recorded as standing debt. Cap the always-loaded contract at 9,000 words and require prune-or-trigger before a crossing change lands. Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com> * docs(vision): restore accepted three-sentence vendor-semantics form (kunchenguid#4873) Replace the compressed paraphrase with the issue's accepted wording: a named quarantined version-pinned adapter, expected to break, recorded as standing debt that never hardens into a shared contract. Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
…or-owed gate (kunchenguid#4974) * fix(watch): recheck a gate awaiting a human instead of wedge-escalating it A lane whose validation run is parked at a gate waiting on a human decision is correctly quiet, but nothing in its status line says so: the evidence is the pipeline's own gate state rather than anything the worker wrote. The wedge timer read that silence as a suspected wedge and climbed the escalation ladder for as long as the wait lasted, and each escalation cost a supervising turn. The landed declared-wait consult does not reach it, because a live ordinary crewmate never reports a declared pause, and raising FM_STALE_ESCALATE_SECS would delay genuine wedge detection for every lane by the same amount. The threshold now reads a second, independent record when the status line accounts for nothing: whether the crew's current state is a gate whose answer is owed by a human. That is minted only from the gate's own findings table, by a row whose `action` column is exactly `ask-user`, located by position out of the table header the way nm_gate_step_row already reads its row - never searched for over the run payload, where a finding's free-text description or a branch name satisfies a search just as well. A gate awaiting the CREWMATE's own answer keeps the unchanged escalation schedule, reason and demand-deep-inspection wording, because a crewmate that goes quiet before answering its own gate is exactly the wedge the ladder exists to catch. Each kind of wait now carries the human it is on, the action that clears it, and whether that human is the captain as data alongside the verdict, rather than as wording chosen per branch where the recheck is written, so the deferral cannot word one kind of wait as another and a new kind cannot ship without deciding all of them. A parked gate has no written record of when its wait began, so its recheck publishes no wait age at all rather than one read from the quiet window this deferral resets on every pass, which would report the same small number for a gate of any age. Like every other captain-facing recheck here it is absorbed in silence while the away-posture record exists, arming no throttle, so the recheck is owed in full the moment the record is archived. The consult runs only in the at-threshold branch that was about to escalate, beside the worktree walk already there, and only for lanes whose status line explained nothing. Closes kunchenguid#3055 * no-mistakes(review): require an unanswered decision before deferring a parked gate * no-mistakes(review): reset the away-silenced timer, fail-safe findings parse, US-joined wait records * test(watch): pass the pane hash wedge_timer_check now takes Upstream gave wedge_timer_check a sixth <pane-hash> argument for its dead-record probe. The malformed-wait-record rounds drive the real function directly, so they pass one, and stub fm_backend_agent_state to a live agent so the probe that runs after a refused deferral keeps the unchanged ladder rather than reading a backend the child shell has none of. * no-mistakes(review): Bind parked-gate wait to its run, owe it firstmate * no-mistakes(document): correct wait-kind count, crew-state reader scope, gate-key coupling * feat(watch): make the parked-gate wait deferral opt-in The wedge timer deferring a lane parked at a validation gate is new supervision behaviour rather than a restored one, and it decides which lanes give up the escalation ladder, so it now ships as a default-off per-home option instead of changing every home on upgrade. config/wedge-defer-parked-gate arms it. The flag is read before the decision fold, so an unconfigured home spends no fold or current-state read, writes no record, and keeps the unchanged escalation schedule, reasons and demand-deep-inspection wording; a test counts the reader calls in both directions to pin that. It is not inherited by secondmate homes: each home supervises its own crew and owns that trade separately, the same reason config/turnend-churn-absorb is home-local. The away-posture absorb returns to leaving the idle timer alone, which it had restarted only because the costly consult could reach it. A parked-gate wait is owed to the supervisor rather than the captain, so it never enters that branch, and the recheck owed on return is again owed in full the moment the record is archived. * test(watch): pin that the away-silenced hold leaves the idle timer alone The absorb no longer restarts the timer, so the recheck owed on return is owed in full rather than a cadence into the return. Nothing asserted that, so a restart could be reintroduced silently. * no-mistakes(review): document away-silence rationale, pin captured gate component * no-mistakes(test): anchor gate row scan to the braced findings header * no-mistakes(document): pin same-block gate row invariant in crew-state comment
…uid#5007) * fix(control): let the owning seat reclaim a task whose endpoint is gone A destroyed pane or workspace made `missing` a terminal state. Relaunch accepted only `dead` and said to stop the agent first; exit refused `missing` and said to reconcile the task first; there is no reconcile verb. Each command named the other as its prerequisite, so a task whose terminal went away could not be reclaimed by anything, and a no-mistakes approval it was parked on had no seat left to answer it. `missing` is agent-free a fortiori: there is no endpoint, so there is no agent in it. Widen the existing guards rather than add a verb. - fm-spawn --relaunch accepts a positively proven `missing` and creates one fresh endpoint in the recorded worktree; the record it already republishes rebinds the task to it. A `dead` endpoint is still adopted in place. - fm-control exit reports `endpoint-gone` instead of dying, so the relaunch transaction's stop step no longer dead-ends, and re-resolves the endpoint from the record before verifying the replacement. The duplicate-agent refusal is untouched: both verdicts come from the same recovery-grade classifier, which claims `missing` only from positive absence, so `alive`, `ambiguous`, and `unreadable` all still refuse. The backends' own create paths refuse a live same-labeled endpoint as a second independent guard. The worktree, its branch, commits, uncommitted changes, armed poll and registration, record rows, and status log are all untouched - a reclaim is a recovery, never a teardown. A secondmate is excluded: its gone-endpoint recovery already has one owner in the session-start liveness sweep, so relaunch refuses and names it rather than becoming a second path to the same outcome. Tests reproduce both halves of the deadlock, the reclaim succeeding, unlanded work surviving it, and the refusals that still hold. * no-mistakes(review): prove endpoint absence per backend before reclaim rebinds * no-mistakes(review): give exit and relaunch one absence proof; pin herdr rebind session * no-mistakes(review): narrow endpoint reclaim to herdr; tmux refuses honestly * no-mistakes(review): stop refusals and docs asserting unestablished causes * no-mistakes(review): stop herdr fixture helper losing tmp-root registration * no-mistakes(review): document workspace drift and absence-probe server residue * no-mistakes(review): correct rebind limitation to its one reachable case * no-mistakes(review): stop claiming reclaim leaves instructions untouched * no-mistakes(document): scope fm-control-lib purity claim, note reclaim coverage * no-mistakes(rebase): read the staged launch file in the herdr fixture Rebasing onto main picked up kunchenguid#4994, which stages a long worker launch command into a script and delivers the short `. '<path>'` line instead of the literal command. The tmux fake and tests/fixtures.sh were updated for that; the herdr fake this branch adds was written before it and still keyed "an agent now exists on this pane" off the literal `encode launch-brief` text, so after the rebase it never marked the rebound pane live and the reclaim's alive-wait read `dead`. Dereference the staged file first, exactly as the tmux fake above does. Test-fixture only; no production path changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * no-mistakes(document): note reclaim placement in herdr and scripts inventories --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…3764) * test(status): reproduce missing event emission time * wip(status): preserve optional event emission time * test(status): document indirect clock stub invocation * no-mistakes(review): Preserve historical status bytes during reply recovery * no-mistakes(test): Fix timestamped status assertions and remote fixture dependencies * no-mistakes(review): Preserve captain regex overrides for timestamped status events * no-mistakes(document): Clarify status event timing and publication contracts * no-mistakes(lint): Quote literal done to satisfy ShellCheck * no-mistakes(ci): Captain, updated .github/workflows/ci.yml to expect 19 snapshot tests instead of 18, matching the PR’s added regression. Reproduced the failure before the fix. Stock Bash 3.2.57 verification passed: parse sweep, 19 snapshot tests, 53 Bearings tests, and the public-followup regression. Workflow lint and diff checks passed * no-mistakes(test): Preserve terminal notifications with malformed timestamp tags * no-mistakes(test): Stamp Rovo spawn failures with emission time * no-mistakes(document): Verify status event documentation * no-mistakes(lint): Fix ShellCheck quoting in status emission-time tests * no-mistakes(ci): Captain, fixed four lifecycle assertions to accept emission timestamps while preserving publication and retry checks. Reproduced the CI failure before the fix. The lifecycle suite now passes with six Beads capability skips; syntax, targeted ShellCheck, and diff checks passed * no-mistakes(ci): Captain, fixed malformed timestamp colons hiding actionable events using shared normalization. Original bytes and unknown ages are preserved. Regression reproduced before the fix; classifier and remote-reply suites, targeted lint, syntax, and diff checks passed * no-mistakes(review): Stamp remote escalations at call sites, drop new flag * no-mistakes(review): Accept stamped escalation and close lines in test assertions * no-mistakes(review): Restore reserved-key answered-note guard for stamped closes * test(status): accept optional emission time in PR-provenance assertions The kunchenguid#4148 provenance test landed on main with exact unstamped greps. Parent-channel lines from this branch carry [at=<epoch>], so strip only that tag before the same exact match. No production change. * no-mistakes(review): Accept stamped ready signal in PR fallback scrape * no-mistakes(review): Drop relay flag, stamp parent events at call sites * no-mistakes(review): Stamp worker terminal-signal instructions, revert fm-on fixture * no-mistakes(review): Accept optional stamp in live cmux drift guard * no-mistakes(review): Restore original test invocation order in two suites * no-mistakes(review): Strip only well-formed numeric status time tags * no-mistakes(document): Drop stale unstamped PR-ready line spelling from channel doc * no-mistakes(review): Stamp agy spawn-failure status lines with event time * fix(bin): normalize status event times in-shell and freeze the budget test clock Two paths made a status event's emission time cost more than it should. The captain-relevance fallback piped every line through awk to drop a well-formed `[at=<epoch>]` tag before matching, so a supervisor sweep paid a fork per line just to prepare a regex match. Shell parameter expansion does the same strip with no fork, and the retry-dedup scan now reuses that one helper instead of carrying a second copy of the rule in awk. The copies had already drifted: the shell side stripped tags from lines with no colon, which the awk rule left whole, so a colonless line could be mistaken for one already recorded. One definition, checked against the awk rule it replaces over the edge cases and a 4000-line fuzz. tests/fm-contributions.test.sh froze its fixture clock only in exhaust mode. In hang mode the poll set DEADLINE to the real now plus a one-second budget, and when the second ticked before the first forge call the loop broke without ever calling gh: forge/calls was never written and the assertion failed reading a missing file. Freezing the clock in both modes removes the dependence on wall time; the bounded call is still cut by the real timeout, so the observation the test asserts still starts. Emission time stays optional on new status records, and legacy or malformed lines keep an unknown age. * no-mistakes(review): Stamp ask-user escalation line and fix Kimi status assertion * no-mistakes(document): Drop stale unstamped done-line spelling from watcher docs * test: fold emission-time snapshot coverage into the fixture case Drop the incidental ci.yml 18-to-19 count hunk so the PR no longer touches workflows. Keep every emission-time assertion by folding it into test_fixture_snapshot_json. * no-mistakes(review): replace brief date substitution with epoch placeholder; drop emitted_at_epoch * no-mistakes(review): align untimed normalizer with epoch parser; tolerate placeholder stamp in PR scrape * no-mistakes(review): strip undelimited at-tags; correct brief stamp header * no-mistakes(review): normalize stamps at both captain-regex sites; restore mtime freshness * no-mistakes(review): strip colon-bearing stamps for relevance; fix headers and test oracles * no-mistakes(review): narrow escalation match to stamp tolerance; pin note verb * no-mistakes(review): read note and key past colon-bearing stamps * test(status): keep inactive reconcile assertions stamp-tolerant These two oracles were made stamp-tolerant while resolving one of the branch's merges from main. The rebase drops merge commits, so that adaptation was lost and both assertions went back to matching an exact substring that a stamped line no longer contains: the tag lands before the colon, so "failed [key=k]: ..." is now "failed [key=k] [at=N]: ...". Strip a well-formed tag before matching, as the branch's other oracles do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * no-mistakes(review): unstamp fold colon tests; reserve stamp width in cap * no-mistakes(document): correct stale unstamped status-line spellings in docs * no-mistakes(document): quote brief-test literals for lint; correct stamp-helper contract comments * no-mistakes(ci): rename subshell-local epoch in delivery-race stub The serialization test overrides fm_pending_reply_mark_delivered inside a (..) subshell. Its `epoch` local collided with the same name in status_line_at_epoch/status_stamp_line, which this branch added and this suite now calls at top level, so ShellCheck 0.11.0 reported SC2030 and failed Lint 2. The stub already prefixes its other locals with `pending_` for the same reason; `epoch` was the leftover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: ship clean Lavish host fixes * no-mistakes(review): Fix Lavish classifications and fail-closed host loading * no-mistakes(review): Restore Lavish host state across retries and launches * no-mistakes(review): Preserve destination Lavish host when configuration is absent * no-mistakes(document): Document Lavish status and host guarantees
…#5076) * feat(afk): make the captain's away words the whole mandate Retire the clause fields, verb list, never-set scan, refused records, and the per-task merge-grant list from the away-posture record. The record is now version 2: the captain's words verbatim plus expected return, spend cap, and reach line; a version 1 record still validates, reads, and archives so a live away window is never broken by the upgrade. The supervision branch reads the words at the tail of every wake and acts on them by its own judgment through the guarded scripts under standing authority, never by analogy, holding for the return on doubt, and opens each such outcome summary with "per your away instructions:" so the return brief can render the words beside the session's account. While the record exists any green merge runs under away authority (ledger tag "away"); red merges, --allow-red, asynchronous and queued merges, and local-only landing stay refused. The branch may file a backlog item the words explicitly call for before dispatching it under the spend cap. Tests drive fm-afk-contract.sh, fm-afk-launch.sh, fm-afk-return.sh, and fm-pr-merge.sh as commands: version 2 written, version 1 read, retired flags and subcommands refused by name, green merges landing under the record, red and waived-red refused, the record lock still closing the authority-read window, and the Pi away tail carrying the words. * no-mistakes(review): carry the away read-back to the session verbatim * no-mistakes(review): match the exact away-action marker in the return brief * no-mistakes(review): refuse a words block truncated by a damaged line * no-mistakes(document): Refresh away-role contract documentation
…unchenguid#5049) * fix(bin): render the remote charter's steering-inbox path host-local A freshly provisioned remote secondmate read a parent-home absolute steering-inbox path in its charter - a location that exists on no route - and spent its first turn discovering the gap and filing a blocked decision for what was a render defect. The seed's remote-copy rewrite now maps the inbox to the route's host-local parent-route inbox, exactly as it already maps the reply-log path, so every mention - bare path, listing, and handled/ acknowledgement - lands host-local. Both rewrites also become plain assignments, because a quoted substitution nested inside a double-quoted printf argument leaks literal quotes into the replacement text on stock macOS bash. The lifecycle suite pins the corrected render both directions against the real seed, provisioning, and delivery route, sharing one fixture value between the render truth and the delivery truth. Closes kunchenguid#5012 * no-mistakes(document): document remote charter's host-local steering inbox
) * feat(procevent): route worker-owned Lavish rounds * no-mistakes(review): drop duplicate artifact field from task-owned registration * no-mistakes(review): post worker reply once, fix ring label, keep re-arm atomic * no-mistakes(review): keep worker board owned until terminal round acknowledged * no-mistakes(review): refuse every retirement of an open worker-owned round * no-mistakes(review): use real lavish reply flag, isolate reply generations * no-mistakes(review): drop .posted marker for best-effort reply posting * no-mistakes(review): consume staged reply after listener setup, refuse orphaned captures * no-mistakes(review): require a reachable owner, redeliver open rounds, roll back failed re-arms * no-mistakes(review): re-arm only to acknowledge an open round * no-mistakes(review): conclude only a still-open terminal round * no-mistakes(review): record the acknowledgement before retiring the board * no-mistakes(review): retain the registration across a conclude, qualify terminal docs * no-mistakes(document): Document worker-owned Lavish round lifecycle
…unchenguid#5107) * fix(bin): reserve contribution observation budget * no-mistakes(review): Strengthen slow-read regression test to exceed the poll budget
…ness JSON (kunchenguid#5103) * feat(bin): add idempotent inbox orders, receipts, replies, and readiness Let a caller supply a request id when publishing a captain inbox note so a retry returns the original note instead of creating a second one, including across the crash window between save and wake announcement. Separate saved from announced so a failed wake is repairable without enqueueing again. Add bounded receipts JSON with omission disclosure, a durable primary reply against a note id, and a read-only readiness projection that can say unknown instead of inferring liveness from a lock file. * no-mistakes(review): fix(bin): honest inbox announce, reply cursor, and readiness verdict * fix(bin): resolve ready from lock-holder ancestry; drop lock status --json Remove the extra JSON surface from fm-lock.sh so its human status still always exits zero. Have the readiness projection classify the inspected home from the lock-holder pid via fm-harness.sh ancestry, with an explicit FM_SUPERVISION_MODEL still winning and an unknown model when there is no holder. Prove the yes path when that ancestry names a known harness. * no-mistakes(review): Harden inbox announce, receipts reads, and reply sequence cursor * no-mistakes(document): Note read-only lock inspection in scripts inventory * no-mistakes(lint): Pass missing id argument to malformed-reply test printf --------- Co-authored-by: cliflacata-svg <304148223+cliflacata-svg@users.noreply.github.com>
…ending text (kunchenguid#5118) * fix(composer): stop a harness footer row from reading as a composer holding text A harness draws its own furniture below the composer - a user statusLine, a permission-mode hint - and the cursorless "bottom-most shape wins" rule looks exactly there. `→` (U+2192) is Cursor's prompt glyph but ordinary text everywhere else, so a statusLine opening with `→` was selected as a bare composer, swallowed the hint row beneath it as wrapped input, and answered `pending` on a visibly empty pane. `fm_task_inbox_ring` defers on exactly that verdict, and `bin/fm-watch.sh`'s re-ring calls the same function, so the first doorbell and every retry were skipped and the worker never saw the steer. Measured live on 2026-09-20: three of five Claude Code 2.1.236 worker panes on Herdr 0.8.0 had genuinely empty composers and every one of them was refused. A separator pair that closed over a bare agent-glyph row is a proven composer container, so the contiguous non-blank rows below its closing rule are that composer's footer and are no longer composer candidates. The demotion is bounded by all three of its own preconditions: a blank row ends the zone, a pair that closed over no glyph row demotes nothing, and a shape with no separator pair at all (Cursor's half-block rules) is untouched. Real unsubmitted text in that same composer, including a stray SGR mouse report left by a click in the pane, still reads `pending`. Pinned by two portable regressions and by a new cursorless arm on the live composer-matrix guard, which re-reads each harness's already-proven-idle pane the way every non-tmux backend reads it and fails naming the harness and version when that read is `pending`. * no-mistakes(review): make composer footer-zone demotion shape-independent * no-mistakes(review): make footer-zone demotion refuse-only and drop rescan * no-mistakes(lint): quote probe-absent sentinel to clear ShellCheck SC2100 --------- Co-authored-by: Koen Muller <koen@catapult.nl>
…5115) Co-authored-by: guanchengh-lgtm <271917158+guanchengh-lgtm@users.noreply.github.com>
… an unreadable runs table (kunchenguid#5114) * fix(bin): stop misreading a no-run branch as an unreadable runs table Defect: when `no-mistakes axi status`'s overview is truncated (a task's own branch has zero rows among the shown ones), fm_nm_select_run's Python fallback derived the repo identity for its direct SQLite query from a `repo: <path>` line it expected in the overview text. The real CLI never emits that line, truncated or not (see the genuine capture at tests/captures/no-mistakes-v1.70.1/overview.toon, which has only `count:`/`runs[...]:`), so the lookup always failed and reported "unreadable runs table" for a task that simply has no run on its branch. On a fleet with many concurrent runs, every idle-branch task hits the truncated-overview path routinely, so this fired every few minutes and drowned genuine unreadable/blocked verdicts in noise. Fix: derive the repo identity from the task worktree path instead, which is exactly the value `no-mistakes` records as a repo's `working_path` (confirmed against the existing capped-overview test fixtures, which already register repos by worktree path). A worktree path that is not absolute cannot be matched and still reads as unreadable rather than being guessed at. Also raise the reader's SQLite busy timeout from 1s to 30s so ordinary lock contention on a busy fleet cannot masquerade as an unreadable database. Safety: every other verdict byte-for-byte unchanged - the repo lookup still requires exactly one matching row (a genuinely corrupt or mismatched repos table still reports unreadable, per the existing `repo` failure-mode test), the branch query and row validation are untouched, and a zero-row result for the branch still flows through the same recursive re-parse that already turns an empty `runs[0]{...}` table into `absent`. Added a regression test (test_capped_overview_without_repo_line_and_no_runs_reports_absent) that reproduces the real overview shape - capped, zero rows for the task's branch, no `repo: ` line - and asserts the crew state falls through to the pane/busy verdict instead of reporting unknown or "unreadable". Full fm-crew-state.test.sh suite passes unchanged otherwise. * fix: recovered same-branch inventory awk misreads empty result as unreadable fm_nm_select_run's deep SQLite reader rebuilds a `count:`/`runs[...]:` overview and re-runs it through the same awk selection pass. When that rebuilt inventory has zero rows for the branch, the row-matching loop never executes, so its counters (`seen`) stay at awk's uninitialized empty string while `expected` and `shown` are plain strings parsed from the header text. Comparing an uninitialized value against a non-numeric string uses string comparison, so "" != "0" is true, and the END block takes the "unreadable runs table" branch instead of falling through to the correct "absent" verdict for a branch with genuinely zero runs. Coerce the affected END comparisons with `+0` so they are always numeric, matching seen/expected/shown/total regardless of whether awk classified them as strings or numeric strings. A truncated or genuinely malformed inventory still differs numerically and still reports unreadable. * no-mistakes(review): bound capped-overview inventory reader and canonicalize worktree lookup * no-mistakes(review): match recorded repo path first, tolerate duplicate spellings * no-mistakes(review): revert repo lookup to exact working_path match * no-mistakes(document): note state-db inventory read under crew-state nm timeout
…ort (kunchenguid#5141) * fix(bin): require a non-draft pull request before a PR-based done report A PR-based ship could report done, and merge monitoring could be armed, while the pull request was still a draft. A draft cannot be merged, so the poll waited for an event that could not occur and nobody was asked to merge. The PR-based definitions of done now require reading the pull request back from the forge and confirming it is not a draft, and a lane that deliberately holds a draft declares a wait instead of done. bin/fm-pr-check.sh refuses to arm merge monitoring on a draft, naming the draft state, and treats an unreadable draft state as before. The draft reading now lives in bin/fm-pr-lib.sh and bin/fm-pr-merge.sh uses it, with its refusal to merge a draft unchanged. Closes kunchenguid#4757 * fix(review): Skip arm-time draft refusal when fm-pr-merge records metadata
* fix(bin): accept quota-axi schema 6 snapshots keyed by provider + accountKey quota-axi 0.1.47 emits schemaVersion 6 once a provider expands to more than one account: every provider row carries an accountKey and one provider id may appear on several rows. fm_quota_json_valid accepted only schema 5 with unique provider ids, so fm-dispatch-resolve.sh, fm-quota-choose.sh, and fm-procevent-quota.sh all rejected the live snapshot and quota-informed dispatch was dead against the current tool. - bin/fm-quota-axi-lib.sh: the validator accepts schema 6 with accountKey required on every row and uniqueness on provider + accountKey; schema 5 keeps its exact rules. FM_QUOTA_ROW_JQ is the one join every consumer uses: schema 5 binds by provider alone, schema 6 binds to the row keyed by the candidate's Pi lane, else the provider's default row, else no row (unmeasured, never blocked, never by position or summed across accounts). - bin/fm-quota-choose.sh: accepts schema 6 JSON and the TOON accountKey column, and joins through the shared function. - bin/fm-dispatch-resolve.sh and bin/fm-procevent-quota.sh: join through the shared function; an expanded provider with no row for the candidate's account is reported as such. - tests: schema 6 fixtures shaped like the real snapshot, each paired with a schema 5 case on the same path; every new case fails on the previous scripts and passes now. - docs: the two sentences naming the row join describe the schema 6 key. * no-mistakes(review): Fix native Codex quota and expanded provider watches * no-mistakes(review): Align native Codex account matching across dispatch paths * no-mistakes(document): Align quota documentation with account-aware snapshots * no-mistakes(document): Align quota dispatch documentation with account matching * fix(bin): keep CI lint and the quota watch test portable - bin/fm-quota-axi-lib.sh: FM_QUOTA_ROW_JQ is read only by the scripts that source this library, so full-mode ShellCheck reported SC2034 on the assignment; mark it alongside the existing SC2016 disable. - tests/fm-procevent-quota.test.sh: the schema 6 provider-watch assertions used rg, which CI runners do not install, so the case failed with 'rg: command not found' rather than on behavior; use grep like the rest of the file. * no-mistakes(document): Documented schema-version account-row compatibility
* test: repair Claude live auto-arm regression * no-mistakes(review): Assert SessionStart digest completeness within its hook_response event * no-mistakes(document): Consolidate Claude live verification references
Roll the shared require-no-mistakes action to the tagged v1.80.1 SHA and grant pull-requests: read so the check can read PR bodies.
…nchenguid#5174) * fix: preserve Pi watcher ownership across session replacement * no-mistakes(document): Scope Pi predecessor retention away from omp * no-mistakes(ci): Diagnosed all three failing checks; only one was code-caused. (ci-3, genuine) Stock macOS Bash snapshot compatibility: `tests/fm-pi-watch-extension.test.sh` failed the macOS Bash 3.2 `bash -n` parse sweep with `line 4265: unexpected EOF while looking for matching '`. I built GNU Bash 3.2.0 from source locally and reproduced it. Root cause: the PR added a comment containing an apostrophe (`// Replacement shutdown deliberately retains module 2's established arm until`) inside a quoted here-document (`<<'EOF'`) nested inside a `$(...)` command substitution. Bash 3.2 has a parser bug (fixed in later bash) where an unmatched single quote inside such a here-doc body is treated as opening a shell quote and never closed, aborting the whole file parse. The base commit parses cleanly under Bash 3.2, confirming this PR introduced the break. Minimal fix: reworded the comment to remove the apostrophe (`... retains the established module-2 arm until`), preserving meaning. Verified `bin/fm-lint.sh --list-files` (the 6 changed shell files) now all pass `/tmp/bash-3.2/bash -n`; Bash 5 also parses. (ci-1, infrastructure) Behavior portable serial 8: GitHub API shows the `Run portable serial shard 8` step conclusion=success; only `Upload portable serial shard 8 timing artifact` failed with `Failed to FinalizeArtifact ... (403) Forbidden`. This is a transient artifact-service/cancellation failure, not a test or code failure. No change. (ci-2, infrastructure) Lint 1: fetched the job log via the GitHub API; it ends with `##[error]The runner has received a shutdown signal...` then exit 143. The step was cancelled mid-run, not a ShellCheck finding. Independently ran `bin/fm-lint.sh --partition 1of2 --telemetry ...` locally with pinned ShellCheck 0.11.0 and actionlint 1.7.12: exited rc=0 (no findings). No change. The only code change is the apostrophe removal in tests/fm-pi-watch-extension.test.sh; no other files modified
…d#5236) * fix(bin): retire windowless leftovers and stop claiming a Pi daemon teardown Catch-up correctly refuses while a leftover task record has no status file. Cleanup used to deadlock on those same records when they also had no spawn_gen and no window, so they lingered and wedged every later away-mode return. Teardown now treats a windowless leftover as a missing-endpoint legacy record, and stop reports that no daemon terminal was running when none was launched. Co-authored-by: Cursor <cursoragent@cursor.com> * no-mistakes(review): Narrow windowless teardown exception to tmux legacy leftovers * no-mistakes(review): Validate windowless leftover identity via shared endpoint validator * no-mistakes(review): Refuse windowless leftovers carrying other backends' endpoint identity * no-mistakes(document): Clarify windowless teardown retry documentation --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…ommands (kunchenguid#5963) * fix(composer): read a titled Claude top rule as the composer's edge A named Claude Code session draws its title into the composer's top rule. The strict separator predicate rejected that row, so the closing rule read as a lower unmatched separator and an idle, empty composer classified unknown on every cursorless backend, refusing fm-send, exit, and relaunch. Spare a bare agent-glyph row sandwiched between a width-proven titled rule and the screen's only unmatched separator directly below it. The strict separator predicate, dead-shell rule, and blank-row posture are unchanged. Fixes kunchenguid#5601 Fixes kunchenguid#5558 * no-mistakes(test): Keep Claude's grey slash command in Herdr payload proof * no-mistakes(test): Make missing-herdr version check hermetic to installed herdr
…#6387) * fix: pre-approve Pi trust for seeded secondmate homes Unattended first launches of Firstmate-seeded Pi secondmate homes stalled on "Trust project folder?" until Enter. Probe --approve like --tui-mode and pass it only for --secondmate when help advertises it (.fm-secondmate-home signal), leaving ordinary workers and older Pi unchanged. * no-mistakes(document): Consolidate Pi seeded-home trust documentation ownership * no-mistakes(ci): Fixed Lint 1’s unused polling counter. Diagnosed Behavior portable serial 4 as a pre-existing delta-reader test clock race; replaced timing-dependent rewrite and deletion with deterministic executable-boundary synchronization. ShellCheck, Bash syntax checks, and git diff --check passed. Delta-reader tests passed three consecutive runs; all three live Pi trust cases passed. Production behavior unchanged
…--external-sources (kunchenguid#6443) * fix(lint): retry memory-bound roots without external sources * no-mistakes(review): Make fallback tests portable and correct source-following telemetry * no-mistakes(review): Remove committed parity fixtures and use disposable test roots * no-mistakes(document): Document ShellCheck memory fallback and telemetry * no-mistakes(review): Cover bounded and unbounded fallback RSS behavior * no-mistakes(document): Correct stale lint fallback documentation * no-mistakes(document): Correct stale lint test documentation * no-mistakes(ci): The memory fallback (the retry without --external-sources) now gets only the time left in its root's original deadline, so it can no longer outlast the CI job. Invariant: one root's first attempt plus its fallback must fit inside a single FM_LINT_ROOT_SECONDS deadline, plus the cleanup grace. Only one site started a new deadline: the fallback call in fm_lint_run_root. The deadline is the only budget involved, because the memory limit already applies to each process separately. Changes in bin/fm-lint.sh: - fm_lint_exec_root now takes a <seconds> argument instead of always reading FM_LINT_INTERNAL_ROOT_SECS. - The first attempt passes the full deadline. - The fallback passes floor((start + deadline - now) / 1000) seconds. - When bounds are enforced and less than 1 second is left, no retry starts. fm_exec_timed rejects 0 seconds, so the retry cannot run with no time. The root keeps reason=memory, and the shard output says "no time left in its Ns deadline to retry without it". - Unbounded local runs have no deadline and behave as before. - The header comment now describes the shared deadline. Changes in tests/fm-lint.test.sh: a new test, test_memory_fallback_spends_only_the_remaining_root_deadline, runs only on hosts that can enforce bounds. It uses a 6 s deadline and 1 s grace. - Case 1: the first attempt runs 3 s and then fails with memory status 251. The test asserts one fallback ran, reported reason=timeout, and the root's recorded duration is under 7000 ms. - Case 2: the first attempt runs 5.2 s. The test asserts no fallback starts, the skip is explained, and the sidecar records memory with source-following 1. Verification: - Full `nice -n 10 bash tests/fm-lint.test.sh` passed, including the new test, in about 5 minutes. - Case 1 run against the HEAD script: the root took 9168 ms, so the under-7000 ms check fails before the fix. - `bin/fm-lint.sh bin/fm-lint.sh tests/fm-lint.test.sh` reported no findings. - The CI workflow is unchanged, so the Test step still runs only tests/fm-lint.test.sh with nice -n 10 and the 12 GiB ShellCheck limit
…tension log opt-in (kunchenguid#5489) * Fix Pi watcher successor-gap confirmations and add extension log Accept an already-acknowledged handling confirmation as a no-op when the generation matches, confirm the restoration's own recovery token with a superseded (not rejected) outcome on generation mismatch, retire an arm on confirm failure only when the failed token names that exact pid, and record restore attempts, readiness timeouts, and confirm results in the bounded state/.watch-extension.log. Regression tests: already-acked no-op plus mismatch/dead-pid/lock-mismatch rejections and the manual-restart churn contract in fm-watch-arm.test.sh, and a mid-restore marker advance with no rejection appendix in fm-pi-watch-extension.test.sh. * Treat a dead arm child as an empty slot so repair and retry recover startArm and scheduleRetry answered unchanged while holding a ChildProcess whose OS process was already gone but whose close had not fired, so neither the repair tool nor the retry timer started anything until that close fired. Gate slot occupancy on a liveness check (exit/signal codes plus pid probe) and start a fresh arm instead, with a regression test driving the repair tool against a dead-but-unclosed child. * no-mistakes(document): Document new Pi extension log knob * no-mistakes(review): Fix confirm-failure retire token match, add distinct-pid test * no-mistakes(document): Clarify retire guard needs pid and generation * Make the Pi extension diagnostic log opt-in and default-off Only a positive FM_WATCH_EXTENSION_LOG_KEEP_LINES enables state/.watch-extension.log. Unset, empty, non-numeric, zero, and negative values disable logging entirely, so the default run writes nothing and never creates the file. The shared positiveInteger fallback semantics stay untouched for the retry and timeout knobs. docs/configuration.md owns the knob contract and docs/watcher-continuity.md points at it. Tests: the superseded-delivery case runs opted in, and a new case proves unset, zero, and non-numeric values create no log file while delivery still succeeds. * no-mistakes(document): Qualify extension-log coverage bullet as opt-in * no-mistakes(ci): The two reported checks (CI run 36372002913, Require no-mistakes run 36372069208) show conclusion action_required with 0 jobs and no logs because this is a fork PR (RibatTRW/firstmate) and GitHub is holding the workflow runs awaiting maintainer approval; that gate is external to the code and needs a maintainer to approve the runs. While verifying the change locally I found a real defect the approved CI run would hit: the PR's new test test_handling_delivered_rejects_a_superseded_generation failed deterministically. Invariant violated: reopen-announced (a non-successor/manual arm start) mints a fresh recovery generation only when the durable wake queue holds unrecovered work; an announced episode with an empty queue must be left untouched so idle arm starts never churn generations (the [ -s queue ] guard from kunchenguid#4819, relied on by bin/fm-watch.sh:2413 and covered by the append-reopens and announcement-bound sibling tests). The test called reopen with an empty queue and expected churn, so the fix establishes the queued-work precondition (append one wake, re-announce, re-read the generation) before asserting the reopen mints and the old confirmation mismatches. Test-only change, 14 lines in tests/fm-watch-arm.test.sh. Verified: fm-watch-arm.test.sh 25/25 ok on two consecutive runs, fm-pi-watch-extension.test.sh 55/55 ok, and shellcheck reports only one pre-existing warning outside the edited region * no-mistakes(document): Restore blank line in watcher-continuity docs * Route superseded Pi deliveries like confirmed ones and cover the retire guard A superseded handling confirmation now falls through to the normal delivery path, so an accepting supervision branch owns the wake instead of main. Scope the watcher-continuity token-pinned confirmation and narrowed retire rule to Pi, since omp and OpenCode still confirm against the current successor. Add tests that fail when the retire guard, the scheduled-retry gate, or the deferred-close gate is reverted, relabel the churned-generation characterization test, and use a reaped pid for the dead-pid rejection.
…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
The merge took upstream's side of every textual conflict, which left fork-only features half wired where both sides had edited the same lines. Kept, re-attached to upstream's reworked code: - the Jev checks (house-rules and already-exists steps in the brief and definition of done, PR risk line, routine-wake triage for declared waits, commit-check launch setting) and their configuration sections - the project development-branch store for spawn, landing and cleanup - the durable pool lease, now bounded with upstream's timeout helpers - captain notes listed on every drain and `fm-inbox.sh pending` - answer receipts for captain calls whose Done rows were pruned - the launch-environment scrub and the failed-cleanup-step messages - supervisor instructions for these in AGENTS.md and the skills upstream moved the old sections into Dropped because upstream now provides it: - per-task spawn --base and its base= record (upstream --base-branch) - note-head status timestamps (upstream [at=<epoch>] stamp) - contributions poll timeout handling and its triage class - dispatch resolver small questions - the bounded-run helper generalisation - completion-gate inventory replacement and --repair-reason - the CI hold on an old Pi version Tests that covered dropped behaviour are removed, and upstream's new fleet-ledger test gets a lease-capable fake pool.
…d tests - the watcher's Jev triage and stale-bound code call upstream's away_record_present, the new name of the helper they used - the base-branch test quotes its task ids for upstream's stricter lint - the PR-risk test records its fixture head as pushed, which upstream's named-head gate now requires before a pull request is registered - upstream's worker-account test retires the previous fixture task before each spawn, since the pool guard refuses a copy a live record names
Upstream launches a worker by sourcing a per-spawn script instead of typing the whole command into the pane, so the case now shows that script's text and names its per-spawn prompt record before comparing.
…the house-rules case
…e through --base-branch
…d it with two small changes; the full `tests/fm-watch-triage.test.sh` run was still in progress when I had to report, so only the two affected tests are confirmed passing. **Failing test:** `tests/fm-watch-triage.test.sh` stopped at `not ok - could not arm a real PR merge poll for a delivered task`. This is a fork-only test of the fork-only "armed merge poll bounds stale churn" watcher feature. Two upstream changes broke it: 1. **Fixture could not arm.** Upstream's `bin/fm-pr-check.sh` now refuses to arm unless the delivered head is reachable outside the worker copy (`fm_dod_accept_ship_done`). The fixture's meta had no `worktree=`, so arming failed with `named head cannot be verified: worktree missing`. I gave the fixture in `make_merge_poll_home` a real git worktree whose HEAD sits under a remote-tracking ref. Upstream's gate is untouched. 2. **Watcher no longer absorbed the churn.** Upstream changed the poll loop's `last` from `last_status_line` to `status_declared_wait_line`, which is empty for a `done:` line. The fork's `merge_poll_stale_bound` took that variable as its status line, so it never saw `done` and the feature was silently dead after the merge. In `bin/fm-watch.sh` it now reads the task's last status line itself, and the status-line argument is dropped from it and from `terminal_stale_bound`. Upstream's `last=` line is unchanged. **Verification:** - Reproduced the CI failure locally with the exact arm command. - After the fixes, `test_armed_merge_poll_bounds_delivered_stale_churn` and `test_stale_churn_without_an_armed_merge_poll_still_alarms` both pass when run on their own. - `bash -n bin/fm-watch.sh` is clean. - Not confirmed: the tests that come after the previously failing one in that file. CI never reached them, and my full local run had not finished. The cancelled "Behavior portable serial 2" check was not part of this round and I did not touch it
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.
Merge instruction
Merge this pull request with a merge commit, not a squash.
It brings in
kunchenguid/firstmatemain (214 commits, through 5838f10). A squash would drop upstream's ancestry and make the next upstream merge replay all of them as conflicts.What happened to the fork's own work
Upstream won every conflict. Follow-up commits fix only what the merge broke.
Survived (upstream has nothing landed for it)
docs/configuration.mdsections: routine-wake triage for declared waits, house-rules check, already-exists search, finished check, review finding sort, pull request risk level, failed check sort, commit check, escalation screen, helper model pick, intake routing, worker health line..firstmate-base, registrybase=), now only as a default that feeds upstream's--base-branchpath where upstream accepts a named base. It is skipped on Gerrit projects, local-only tasks, scouts on local-only projects, and clones with noorigin. Fleet sync still tracks the declared branch.fm-inbox.sh pending.INHERITED ENVIRONMENTline at session start.Dropped because upstream now covers it
--baseand itsbase=record: upstream's--base-branch.[at=<epoch>]stamp, which the command center now reads.bin/fm-timeout-lib.sh.--repair-reason.Not reconciled: nothing.
To know after landing
tasks-axito 0.2.6.tests/fm-supervision-host.test.sh), so it is runner luck, not this merge; shard headroom is separate follow-up work.Intent
Firstmate runs from our fork, talktejas/firstmate, and updates itself only from that fork.
The fork has fallen behind the original repository, kunchenguid/firstmate: on 2026-10-06 the original's main held 214 commits the fork's main lacks, and the fork held 56 the original lacks.
Merge the original's main into the fork so the fork carries every one of those upstream changes.
Standing rule for this repository: upstream wins every conflict ("for firstmate repo upsteream wins every conflict").
Where upstream has landed code for something we also changed, take theirs; our own work survives only where upstream has nothing landed for it.
It arrives as one pull request into the fork's main for approval.
What Changed
upstream/main(kunchenguid/firstmate, 214 commits) into the fork in one merge commit, taking upstream's side of every textual conflict. This brings in upstream's supervision host and engine, fleet ledger, live lab, host mirror, worker accounts, Devin harness, Gerrit forge detection, pipeline spend report, AI-trailer stripping, the reworked spawn, watch, wake, inbox and captain-hold scripts, plus their skills, docs and tests.--base-branch, and gated where upstream refuses a named base), the durable pool lease, captain notes on every drain andfm-inbox.sh pending, answer receipts for pruned captain calls, the launch-environment scrub, and failed-cleanup-step messages.--base, note-head status timestamps (andtests/fm-classify-status-timestamp.test.sh), contributions poll timeout handling, dispatch-resolver small questions, the bounded-run helper generalisation, completion-gate inventory replacement and--repair-reason, and the CI hold on an old Pi version — and updates fork-only code, tests and docs for upstream's renamed helpers, stricter lint and sourced launch script.🤖 Generated with Claude Code
Risk Assessment
Testing
Checked merge ancestry against the live upstream and fork remotes, ran seven targeted behaviour tests (base-branch spawn, brief, turn-end guard, secondmate restart, pool base freshen, task delivery, remote secondmate lifecycle e2e), and drove the project-base default gate by hand in an isolated temp home. Six tests pass and the manual gate transcript matches every expected case. The remote secondmate lifecycle e2e fails at one timing-bounded case; the same case fails on snapshots of pristine upstream 5838f10 and fork base cdc2e57, so it is a pre-existing host issue left to remote CI. No visual evidence: the change has no rendered UI surface that was exercised.
Evidence: Project base default gate, driven by hand
Source: Project base default gate, driven by hand
plain resolver (nm) -> develop --task-default no-mistakes, no-mistakes project -> develop --task-default local-only -> (empty, exit 0) --task-default '' (scout), local-only project -> (empty, exit 0) --task-default '' (scout), no-mistakes project -> develop --task-default no-mistakes / '' on gerrit project -> (empty, exit 0) --task-default no-mistakes, clone with no origin -> (empty, exit 0)Evidence: Merge ancestry against live remotes
Evidence: Base-branch spawn test log
Source: Base-branch spawn test log
Evidence: Turn-end guard test log
Source: Turn-end guard test log
Evidence: Secondmate restart test log
Source: Secondmate restart test log
Evidence: Brief test log
Source: Brief test log
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
Step was skipped.
tests/fm-session-start.test.sh:1577- Upstream'stest_abnormal_digest_death_banners_and_exits_zerofinds the digest child to kill by looking forFM_SESSION_START_STAGE_FILE=in/proc/<pid>/environ. The fork's surviving change inbin/fm-session-start.sh(lines 335-365) hands the stage file over as a private--_child <file>argument and unsets that variable in the parent, so no process carries the marker. Trace on Linux: the fakepswalks the ancestry, never matches,targetstays empty, nothing is killed, the digest completes normally, and the assertion forSTARTUP TRUNCATED - SESSION START DIED UNEXPECTEDLY (exit 143...)fails. Traced by reading, not run (review does not run tests). Fix: make the fakepsidentify the digest child by its own argv (the bash process runningfm-session-start.sh --_child ..., not thetimeoutwrapper above it), and update the comment at 1555-1565 that still describes the env hand-off.tests/fm-remote-secondmate-lifecycle-e2e.test.sh:1066- Intent: "upstream wins every conflict". Two conflict hunks in this file (lines 1066-1069 and 1131-1134) end on the fork's side. The merge commit took upstream's lines (correlated reread:newest_remote_inbox_corr,await_reply_result ... $SID.2.result/$SID.3.result,fm-procevent-remote-reply.sh handle), then fa41dbf replaced all 16 of them with the fork'sdelivery=/ nocorr=assertions. The reason is real: the fork's fire-and-forget reread nudge (bin/fm-config-push.sh,bin/fm-bootstrap.sh,bin/fm-secondmate-restart.sh,fm_secondmate_nudge_delivery_id) auto-merged and upstream's assertions would fail against it. Decision needed: keep the fork's fire-and-forget nudge with its test (an exception to the standing rule), or drop the nudge change and restore upstream's test lines.bin/fm-spawn.sh:3457- Intent: "Where upstream has landed code for something we also changed, take theirs; our own work survives only where upstream has nothing landed for it." Upstream landed a per-task base (6a7b391,--base-branch,base_branch=in meta) and the fork's per-task--basewas correctly dropped for it. But the fork's project-level development-branch store (bin/fm-project-base.sh,.firstmate-base, registrybase=) was re-attached inside upstream's conflict hunks:freshen_spawn_worktree_base(bin/fm-spawn.sh:3457-3487),bin/fm-merge-local.sh:110,bin/fm-teardown.sh:1938,bin/fm-project-mode.sh --base. Two behaviours now differ from upstream. (1) A spawn with no--base-branchon a project that declares a base resets toorigin/<declared>instead of origin's default, yet records nobase_branch=, so upstream's readers (bin/fm-review-diff.sh:79, the teardown content check atbin/fm-teardown.sh:1181, the worker's PR-target wording infm_dod_block) still use the default branch for a task that started elsewhere. (2) Inlocal-onlymode the worktree resets to the local declared branch with no fetch andfm-merge-local.shlands on it, while upstream'sfm_base_branch_validrefuses any named base for local-only. fa41dbf's message lists this as deliberately kept. Decision needed: is the project-level store 'nothing landed upstream' (keep), or is it superseded by upstream's named base branch (remove the store and its three call sites)?bin/fm-session-start.sh:196- The one conflict hunk in this file (header comment) keeps the fork's wording for the--_childhand-off and the INHERITED ENVIRONMENT report, merged with upstream's new 'ANY nonzero child exit' sentence. The code it describes is fork-only on lines upstream did not touch since the merge base, so the comment matches the code; taking upstream's text verbatim would have described a mechanism the file no longer uses. Noted only as a literal exception to 'upstream wins every conflict'; it is the cause of the test break reported separately.bin/fm-teardown.sh:2367- Checked and clean: the merge's second parent is upstream 5838f10 with all 214 upstream commits reachable, and no upstream file is absent from the final tree. Replaying the merge gives 42 conflicted files; the features fa41dbf says it dropped are byte-identical to upstream (fm-classify-lib.sh,fm-contributions.sh,fm-dispatch-resolve.sh,fm-backlog-transition-lib.sh,fm-secondmate-report.shand their tests). No function removed by upstream or dropped from the fork is still referenced inbin/ortests/. The fork's durable pool lease sits in a spawn region upstream did not change since the merge base, and the fork's two-record slot tie-break was dropped for upstream's equivalent at this line. Only four auto-merged files were hand-edited afterwards (CI workflow and three tests, all removing coverage of dropped features). One capability is lost as a consequence of the rule: the fork's per-task base worked for local-only projects, upstream's--base-branchrefuses them.🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:
bin/fm-brief.sh:308- The fix round's project-base default skips onlylocal-only, but upstream'sfm_base_branch_valid(bin/fm-dod-lib.sh:175) also refuses a named base on any forge other thannone. Sequence: a project registeredforge=gerritthat declares a development branch (.firstmate-baseor registrybase=).fm-brief.sh <id> <proj> --mode no-mistakes --forge gerritwith no--base-branchnow sets BASE_BRANCH from the store and exits withfm-brief.sh --base-branch: a base branch is not supported on forge=gerrit, a flag nobody passed, and there is no opt-out. A scout on the same project scaffolds (brief FORGE is alwaysnonefor scouts) butfm-spawn.shthen refuses at bin/fm-spawn.sh:3143 using the registry forge, and removing the brief's Base branch line does not help because spawn re-applies the store. Before this round such a project scaffolded and spawned (the store was applied in the worktree refresh without validation). Decision needed: should the store be skipped wherever upstream refuses a named base (gerrit as well as local-only, so the task starts from the default branch), or is the hard refusal intended for a Gerrit project that declares a base?bin/fm-spawn.sh:3131- The instruction for this round was "For local-only tasks do not apply the store at all", but the gate tests the task's$MODE, and a scout has no mode (--modeis refused for scouts in both fm-brief.sh and fm-spawn.sh). So on a project registeredlocal-onlythat declares a base, a scout still gets the store: the brief recordsBase branch: developand the meta recordsbase_branch=develop. Two consequences. (1)fm-promote.sh <id> --mode local-onlyreads that meta field and refuses at bin/fm-promote.sh:202 (a base branch cannot ship mode=local-only), so a scout on such a project can no longer be promoted to the project's own delivery mode; before this round the scout recorded nobase_branch=and promotion worked. (2) If the project clone has noorigin, the scout spawn itself now fails infreshen_spawn_worktree_base(has no origin, so it cannot start from base branch), where it previously returned early. The same condition is duplicated at bin/fm-brief.sh:308. Decision needed: should the default also be skipped for scouts when the project's registered posture is local-only (gating on the registry posture rather than the task mode), or should scouts on local-only projects keep the declared base and lose local-only promotion?bin/fm-spawn.sh:3144- Consequence of routing the store through upstream's path, noted for awareness: a brief scaffolded before this change (noBase branch:line) for a project that declares a base is now refused at spawn withmust record Base branch: <declared> ... to spawn with --base-branch <declared>, and must be re-scaffolded. The same refusal fires if the resolved declaration changes between scaffold and spawn. It fails closed and the message names the working remedy, though it refers to a--base-branchflag the operator did not pass.🔧 Fix applied.
2 issues (1 warning, 1 info) still open:
docs/architecture.md:390- Stale sentence left behind by the first fix round. It still says: "For alocal-onlyproject it is equally where work lands:bin/fm-merge-local.shfast-forwards the resolved branch andbin/fm-teardown.shmeasures merged work against that same branch". That round restored both scripts to upstream (bin/fm-merge-local.shis now byte-identical to upstream and lands on the default branch;bin/fm-teardown.sh's local-only check usesdefault_branch), anddocs/scripts.md:88was updated to say "local default branch". The sentence also contradicts the line directly above it, which says a local-only task ignores the declaration. An agent reading this would expect a local-only project that declaresdevelopto land ondevelop, when it lands on the default branch. Fix: delete the sentence (or restate it as: local-only work lands on the repository default branch regardless of the declaration).bin/fm-project-base.sh:58- Checked by reading the code, no tests run. (1) Re-read nudge:bin/fm-config-push.sh,bin/fm-secondmate-restart.sh,bin/fm-secondmate-nudge-lib.sh,bin/fm-merge-local.sh,tests/fm-secondmate-restart.test.shandtests/fm-remote-secondmate-lifecycle-e2e.test.share now byte-identical to upstream 5838f10; the remainingbin/fm-bootstrap.shdifference is only the fork's pool-lease flag check. (2) Project-base default:--task-defaultis the one gate bothbin/fm-brief.sh:309andbin/fm-spawn.sh:3132call. Traced: local-only ship (mode passed, refused byfm_base_branch_valid, skipped); scout on a local-only project (empty mode falls back to the registered posture, skipped in both brief and spawn, so they agree and nobase_branch=is recorded); Gerrit-registered project (registry forge, skipped for ship and scout); clone with no origin (skipped before validation); malformed registry forge (exit 3, skipped). An explicit--base-branchnever enters the gate, so upstream's refusals are unchanged. The three new tests drive a real spawn and assert the worktree HEAD and the persisted meta, and would have failed against the previous round's code. The extra[ "$FORGE" = none ]inbin/fm-brief.sh:308only covers a brief whose--forge gerritdisagrees with the registry, which spawn refuses anyway.🔧 Fix applied.
1 info still open:
bin/fm-spawn.sh:3131- No new defects found; noted for awareness, checked by reading code only (no tests run). (1) Intent conformance: upstream tip 5838f10 is an ancestor of HEAD, no upstream file is deleted, no conflict markers remain, and every changed shell script passesbash -n. Rebuilding a mechanical upstream-wins merge in a temporary object store and diffing it against HEAD shows the manual reconciliation only moves further toward upstream (for examplebin/fm-classify-lib.sh,bin/fm-dispatch-resolve.sh,bin/fm-contributions.sh,bin/fm-merge-local.shand the secondmate nudge scripts are now byte-identical to upstream). (2) Where fork code replaces upstream lines (pool lease inbin/fm-spawn.sh,collect_local_firstmate_statesmoved tobin/fm-wake-lib.sh,fm-send.shexit 4, the--_childhand-off inbin/fm-session-start.sh, the merge-poll stale bound inbin/fm-watch.sh), upstream changed none of those regions since the merge base, so they are fork work surviving where upstream landed nothing. (3) Fix-round code: the--task-defaultgate inbin/fm-project-base.sh:58is the single gate bothbin/fm-brief.sh:309andbin/fm-spawn.sh:3132call; traced a scout (empty mode falls back to registered posture), a local-only ship, a Gerrit-registered project, a clone with no origin and an unregistered project, and brief and spawn agree in each. A declared branch missing on origin still refuses at spawn, as the pre-merge fork did.fm-captain-hold.shreceipts were adapted to upstream's three-field result (<id> pruned-resolution pruned), which the completion gate parses correctly. (4) The last round's doc correction is in place and no other maintained prose repeats the stale landing claim. No post-merge test addition asserts on implementation source text.tests/fm-remote-secondmate-lifecycle-e2e.test.sh:974- This test fails on this host at 'remote spawn never reached its blocked inheritance write' (a 30-second bounded wait), run in parallel and alone. The identical case fails on snapshots of pristine upstream 5838f10 and of the fork's current main cdc2e57, and the file is byte-identical to upstream, so the merge did not introduce it. The correlated re-read assertions later in the file were not reached locally; remote CI must cover them.git rev-list --count 5838f10 ^HEAD(0),git rev-list --count 5838f10 ^cdc2e57(214),git rev-list --count cdc2e57 ^5838f10(56),git merge-base --is-ancestor cdc2e57 HEADgit ls-remote https://github.com/kunchenguid/firstmate main(5838f10, equals the merge's second parent) andgit ls-remote https://github.com/talktejas/firstmate main(cdc2e57, equals the base)git grep -nE '^( | ) ' HEAD(no conflict markers)git diff --quiet 5838f10 HEAD -- <file>for bin/fm-config-push.sh, bin/fm-secondmate-restart.sh, bin/fm-secondmate-nudge-lib.sh, bin/fm-merge-local.sh, tests/fm-secondmate-restart.test.sh, tests/fm-remote-secondmate-lifecycle-e2e.test.sh (all identical to upstream)bash tests/fm-spawn-base-branch.test.sh(pass, 18 cases)bash tests/fm-brief.test.sh(pass)bash tests/fm-turnend-guard.test.sh(pass, 9 grok-guarded entries)bash tests/fm-secondmate-restart.test.sh(pass)bash tests/fm-spawn-pool-base-freshen.test.sh(pass)bash tests/fm-task-delivery.test.sh(pass)bash tests/fm-remote-secondmate-lifecycle-e2e.test.sh(fails at 'remote spawn never reached its blocked inheritance write', in parallel and alone; 16 cases pass before it)Same e2e test ongit archivesnapshots of upstream 5838f10 and fork base cdc2e57 in /tmp (same case fails on both)Manual:bin/fm-project-base.sh [--task-default <mode>] <clone> <project>andbin/fm-project-mode.shagainst a temp home with no-mistakes, local-only, gerrit and no-origin projects✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.