Conversation
…spatch (#1) * feat(harness): add the cline crewmate/scout adapter with ClinePass dispatch Add Cline CLI 3.0.62 as a verified crewmate/scout harness following the agy pattern: ancestry detection on the native .cline process, a launch-then-send TUI launch, per-task .cline/hooks busy/turn-end wiring under a new cline-hook busy source, Escape interrupt, /exit, and control-plane tables. Wire the ClinePass open-weights pool into the crew-dispatch example and document the known composer-empty gap (placeholder luminance above the shared ghost ceiling) with a tmux live guard as the refresh command. * no-mistakes(review): fix(docs,quota): correct cline resume grouping and cline-pass family id * no-mistakes(document): docs: cover cline in tmux liveness/anchoring list and configuration.md secondmate-refusal note * no-mistakes(lint): {"summary": "lint: no code changes needed, fm-lint.sh passes with shellcheck on PATH"} * chore(gitignore): drop the stray .omc handoff artifact and ignore .omc/ The no-mistakes gate agent's Claude Code oh-my-claudecode plugin writes .omc/handoffs/last-session-end.md into the run worktree at session end, and a later pipeline step committed it into this branch. Remove the committed file and ignore .omc/ so a home-environment handoff artifact can never ride into a PR. --------- Co-authored-by: firstmate-worker <worker@local>
Two Claude subscriptions need to run concurrently across lanes without moving every claude spawn onto one account. --claude-config-dir picks the CLAUDE_CONFIG_DIR one claude spawn's pane resolves into: validated before any worktree or endpoint exists, recorded in the task's own meta, reused unchanged on --relaunch, and threaded through both the pre-launch trust registration and the launch's own environment so the two halves can never land in different stores. A spawn naming no seat is byte-identical to before.
…rop stray artifact
Count live crewmate, scout, and local secondmate lanes grouped by the billing provider a candidate actually draws on, expose the load for dispatch intake, and refuse a spawn that would push a provider past its configured cap. Provider identity comes from the resolved model string, not the harness name: a provider-qualified prefix or a model-id pattern decides the pool, and the harness table is only the fallback. That keeps two models on one pool counting together while a different pool stays separate. The mapping lives once in bin/fm-provider-lib.sh, whose fallback reuses the existing quota tables rather than restating them. A lane occupies a seat unless its recorded endpoint is provably dead or missing, so a cleared seat is visible before the next dispatch. The cap comes from providerCaps in config/crew-dispatch.json (per provider, else default), falling back to 4; bootstrap now rejects a malformed providerCaps instead of silently ignoring it. bin/fm-provider-load.sh prints the current per-provider used/cap for intake. Stranded-record detection stays with fm-lane-account-dead-records; this counter reads the current endpoint classifier.
Deliver bin/fm-hold-reverify.sh, an armed watcher check that re-checks each captain hold past an age threshold against shipped reality and reports it as dead, still_live, not_a_decision, or unestablishable - the reconciliation vocabulary captain-hold-lifecycle already owns. It reports only: it never calls answer and never closes or annotates a call, so only the captain's own words or an explicit evidence-backed reconciliation can resolve one. Dead is never inferred from absence or an unreadable source. Aged holds come from the canonical local backlog projection (fm-fleet-snapshot.sh --contribution-input); recorded pull requests are read through fm-pr-lib.sh. Each sweep writes a docket and prints one line only when the finding set changes, with a report record keyed on that set.
…rget
A firstmate home had no way to see that another home was already working a
shared external target, so the main home and a secondmate could both arm to
land the same PR with nothing to stop a double merge.
bin/fm-claim.sh records, releases, and inspects a work claim on a shared
external target - a PR, an issue id, or a declared file area. The store is a
machine-wide directory (FM_CLAIM_ROOT, default
${XDG_STATE_HOME:-$HOME/.local/state}/firstmate/claims), the sibling of the
existing process-event source claim root, because one owner per canonical
target cannot live inside a single home. Local homes share one filesystem; a
remote secondmate is a separate host and stays outside the mechanism.
Acquire is atomic and fails closed: a second home's live claim refuses rather
than racing. A claim is released on cleanup or reclaimed only when its holder
is provably gone (its home directory is absent, or its task record is absent
past FM_CLAIM_PENDING_GRACE). Any uncertainty keeps the claim.
bin/fm-spawn.sh --claim records the claim before any endpoint or task record
exists and refuses the spawn on conflict, recording the canonical keys on the
task as claims=; fm-teardown.sh releases them on cleanup. The flag is refused
on --secondmate, --relaunch, and a batch dispatch.
Tests: tests/fm-claim.test.sh drives the real CLI across two simulated homes
sharing one claim root, plus a real spawn that records its claim and a second
dispatch that is refused.
Merge authority was decided once at intake and never revisited, so a task dispatched yolo=on kept that authority even after firstmate held it for the captain, and the recorded authority and the merge path could disagree. Fold the captain-hold predicate into fm_merge_authority_resolve, the single owner of a task's standing merge authority, so a held task resolves to captain-hold (or hold-unreadable) whatever its yolo posture or away grants say. Remove bin/fm-pr-merge.sh's duplicate require_released_captain_hold and fold its refusal into the shared gate, and have bin/fm-merge-local.sh share the same predicate instead of repeating it.
A worker parked on a provider quota wall kept a live, painting harness while its turn could not advance, so every one of them read as working from its semantic busy record. The measured fleet incident had all of one provider's workers stalled at the same weekly limit while supervision saw a healthy fleet. Recognize the wall from the pane text the busy reader already inspects. The signal is built from two independent rendered families - a wall-shaped limit phrase and a scheduled retry/reset phrase - within the last few non-empty lines, so no single vendor string is load-bearing and ordinary worker prose does not match. A busy verdict over that wall reports `quota` instead of busy. fm-crew-state.sh surfaces it as its own `state: quota` rather than collapsing it into working or a declared pause, because a quota-killed worker cannot be relaunched in place; the recovery skill now states that preserve-and-replace under a new id is the path. The portable regression pins the logic and its divergence cases over synthetic transcripts. The live guard drives the real installed OpenCode TUI against a local 429 stub so its own retry modal renders with no model tokens spent, and proves the same task reads working before the wall and quota after it.
Teardown refused any record whose endpoint was already cleared, so a lane could never be retired once its window was gone and it kept occupying an in-flight row. Accept an explicit endpoint_cleared stamp as stronger agent-less evidence than a dead window, with no flag and no --force, while keeping the unlanded-work refusal unchanged. A projected Herdr teardown confirmed only the task pane was gone, so a workspace whose recorded pane vanished before its close survived for a restart to restore as a live agent in the primary clone. Remove the workspace's remaining panes through the same focus-preserving pane close and require the workspace gone before retiring the journal. A dead pane whose display redrew re-alarmed on every new hash, a supervision tax that grew with each dead lane. Absorb a redrawn dead display against the existing once-record, while a relaunched agent re-arms the incarnation and its own death still reports in full.
…human-read text
…e ship definition of done
…ot wedge a supervisor
… liveness sweep
…control exit works
…nnot race one target
… alongside the reliability batch # Conflicts: # bin/fm-bootstrap.sh # bin/fm-control-lib.sh # bin/fm-quota-choose.sh # docs/configuration.md # docs/examples/crew-dispatch.json
… fork merge Committed only to let a clean merge proceed without touching this work; not otherwise reviewed or authored by this session.
…s the preserved cline adapter # Conflicts: # AGENTS.md
Make the Fireworks DeepSeek dispatch lane usable: openhands is now a verified crewmate/scout harness with launch, readiness, busy, interrupt, and exit mechanics, so config/crew-dispatch.json no longer fails as an unverified adapter.
# Conflicts: # .agents/skills/harness-adapters/SKILL.md # AGENTS.md # bin/fm-agent-process-lib.sh # bin/fm-bootstrap.sh # bin/fm-busy-lib.sh # bin/fm-composer-lib.sh # bin/fm-control-lib.sh # bin/fm-harness.sh # bin/fm-spawn.sh # docs/agent-control.md # docs/architecture.md # docs/configuration.md # docs/trace-context.md # docs/verification/runtime-backends.md # tests/fm-control.test.sh
# Conflicts: # .gitignore # bin/fm-spawn.sh
* fix(bin): read aged holds via backlog-json, not contribution-input The hold re-verify sweep only needs canonical backlog rows; contribution-input also walks every task meta for merge-authority resolution, which took ~96s at this fleet size and always exceeded the five-second projection bound. Add fm-fleet-snapshot.sh --backlog-json for that narrower read and point the sweep at it so failures stay loud without raising the timeout. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(bin): apply review decisions for aged hold re-verify sweep Sort aged captain holds oldest-first before the per-sweep cap, let FM_HOLD_REVERIFY_BUDGET_SECS govern the backlog projection bound, clamp forge probes to remaining budget, skip probes for predetermined not-a-decision rows, drop the classify subcommand, and document --backlog-json. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(bin): use backlog_json output mode label for shellcheck Co-authored-by: Cursor <cursoragent@cursor.com> * docs: fold pipeline document-step hold-reverify pointers into fork tip Adds the toolbelt row and prose alignment from the failed run's document step without rebasing onto upstream main. Co-authored-by: Cursor <cursoragent@cursor.com> * no-mistakes(document): correct hold-reverify docket contents description * chore: gitignore .omc session state and document in AGENTS.md Apply captain inbox 006 on the fork publication branch without rebasing onto upstream main. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: expect 19 snapshot/fleet-view tests under stock macOS Bash The fork's tests/fm-fleet-snapshot-view.test.sh carries test_large_payloads_compose_through_files, the regression for bin/fm-fleet-snapshot.sh composing payloads above 128KB through files instead of argv. It landed in d9356ca together with that snapshot change and passes under /bin/bash 3.2 on the macOS runner, which already counted 19 ok lines, so the hard-coded 18 in the macOS job was the only thing left behind. * docs: declare the cline-pass provider on the documented cline profiles The resolver refuses docs/examples/crew-dispatch.json with "profiles whose harness lacks one authoritative provider family require provider: cline", so the documented-example check in tests/fm-dispatch-resolve.test.sh has failed since the cline profiles were added to the example. The example is the wrong side. The resolver's provider is the quota-axi provider family it ranks candidates by, not the launch prefix cline reads from its model id, and the single-provider table in docs/configuration.md leaves cline out on purpose: the model prefix, not the harness, decides who bills the run, exactly as for pi and omp. The Pi default in the same example already declares provider: claude for that reason. Add provider: cline-pass to the four cline profiles, correct the one sentence in docs/configuration.md that claimed no field was needed, and give the test's canned Choice answer the fourth rule the example now has, since the resolver checks the answer against the full option set. * test: corrupt the claim record under test, not the first one find returns The corrupt-record check picked its victim with find | head -n 1 while three claims exist, so on a filesystem whose directory order differs from the author's it corrupted o/r#7 or repos/example#9 and then asked about owner/repo#11, whose intact record answered "held" with exit 0. CI's stdout showed exactly that line. Select the record by its documented key= line instead. The guard itself is intact: corrupting the right record returns exit 5 on the same inputs. * test: give the restart watchers a refresh bound a slow runner can meet In the watcher-restart section of tests/fm-home-summary-refresh.test.sh the watcher runs with FM_HOME_SUMMARY_INTERVAL=999999, but age_of reports 999999 for a missing ledger, so its detached refresh fires on every one-second poll. After the lock holder is killed that refresh steals the dead lock ahead of the test's idle-only refresh, which then returns without publishing, and with the section's FM_HOME_SUMMARY_TIMEOUT=2 a slow runner kills the watcher's attempt before it publishes. The next poll dies the same way and the ledger never appears, which is the "a dead publication lock wedged publication" failure in CI run 35683307194. Raise the three restart watchers' bound to 30 seconds, the deadline this file already uses for its accumulated-home publication. Under a 30% CPU quota the unchanged test fails on exactly that line and the changed one passes all 21 checks. The idle-only refresh, the dead-lock reclamation and the 10-second wait are untouched.
…wner (adopt kunchenguid#5993) (#7) * Fix reassigned teardown slot collisions (cherry picked from commit 115333b) * no-mistakes(document): Document claim-first pool-slot teardown behavior (cherry picked from commit dcbe51a) * no-mistakes(document): Confirm teardown documentation reflects slot ownership behavior (cherry picked from commit bdcedcf) * fix(teardown): avoid presentation-lock races after claim-first slot cleanup Reassigned-slot teardown with a dead Herdr husk no longer takes the shared presentation session lock, restoring the pre-5993 contention profile for stale records while keeping live-slot protection intact. Herdr presentation recovery spawns now wait up to 120s for that lock and release it on abort so concurrent cross-home recovery cannot fail the 5s try loop after a legitimate holder. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: QIanGua <15757826110@163.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…in config (#9) * Fix OpenCode v2 worker launch to use --standalone and config model. OpenCode 2.x removed the interactive --model flag; carry the resolved model in OPENCODE_CONFIG_CONTENT and launch with --standalone so the model and permission block are honored off the shared service. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(bin): honor opencode v2 top-level model and gate --standalone Always write the resolved model as OPENCODE_CONFIG_CONTENT top-level model on 2.x, drop unverified agent.build variant JSON, and keep the 1.x --model launch shape when opencode --version reports major 1. Co-authored-by: Cursor <cursoragent@cursor.com> * test: stub opencode --version in shared spawn fakebin Spawn tests prepend a fakebin to PATH; fm-spawn now probes opencode --version for the v1/v2 launch gate, so every spawn fakebin must answer it. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Brings the fork up to date with upstream, including the watcher stall and re-arm fixes, teardown retiring watcher markers, orphan journals and wake rows, herdr endpoint reclaim, the shared secondmate liveness library, the worker account pin, the Devin adapter, and the launch-prompt backstop. Conflict policy: upstream's version wins wherever it fixes or supersedes a fork change; the fork's adapters, features and fixes that upstream does not ship are kept on top of it. - Merge authority: upstream's away-record model replaces the fork's captain-hold-revokes-yolo change, so that change and its tests are dropped. Upstream's pr-merge and merge-local already refuse a held task. - The WIP commit of uncommitted local edits is not carried over. - OpenCode: 1.x keeps --model and the effort variant from upstream; 2.x keeps the fork's top-level model plus --standalone launch. - A per-lane Claude config-dir seat and the per-home worker account pin both choose the Claude store, so fm-spawn refuses the combination. - Definition of done: the evidence-pair block is rendered by a wrapper around upstream's reworked per-forge blocks.
…m merge Merge took upstream's --arg jq transport and dropped the fork's --rawfile / --slurpfile staging for >128KB status folds, crew-state detail, secondmate row composition, and contribution-input. CI failed with jq Argument list too long on portable serial 8 and Stock macOS Bash. Port the fork transport onto the merged schema while keeping upstream age_seconds / observed_age.
|
Closed: this PR is intended for the downstream fork keenvc/firstmate (keenvc#11), created against upstream by mistake during automated sync. |
|
| fm_claim_read "$path" || continue | ||
| if [ "$FM_CLAIM_HOME" = "$HOME" ] && [ "$FM_CLAIM_TASK" = "$TASK" ]; then | ||
| if rm -f -- "$path"; then |
There was a problem hiding this comment.
Cleanup can delete another claim
When teardown or an aborted spawn releases a task's claims, release-task checks each claim and removes its file without taking the per-target lock. If another home reclaims a stale claim between those steps, cleanup deletes the new home's live claim. A third home can then claim the same target and dispatch duplicate work.
| if [ "$FM_CLAIM_HOME" != "$HOME" ] || [ "$FM_CLAIM_TASK" != "$TASK" ]; then | ||
| echo "error: release refused - $KEY is held by home $FM_CLAIM_HOME for task $FM_CLAIM_TASK, not by home $HOME for task $TASK" >&2 | ||
| return 3 | ||
| fi | ||
| begin_mutex "${PATH_CLAIM}.lock.d" | ||
| rm -f -- "$PATH_CLAIM" || die "could not remove the claim at $PATH_CLAIM" |
There was a problem hiding this comment.
| while [ "$path" != "${path#./}" ]; do path=${path#./}; done | ||
| path=$(printf '%s' "$path" | tr -s '/') | ||
| path=${path#/} | ||
| while [ "$path" != "${path%/}" ]; do path=${path%/}; done | ||
| [ -n "$path" ] || return 1 | ||
| printf 'area:%s:%s\n' "$project" "$path" |
There was a problem hiding this comment.
| if [ -e "$FM_CLAIM_HOME/state/$FM_CLAIM_TASK.meta" ] || [ -L "$FM_CLAIM_HOME/state/$FM_CLAIM_TASK.meta" ]; then | ||
| return 1 | ||
| fi | ||
| case "$FM_CLAIM_CREATED" in | ||
| '' | *[!0-9]*) return 0 ;; | ||
| esac | ||
| now=$(date +%s) || return 1 | ||
| grace=$(fm_claim_pending_grace) | ||
| case "$grace" in | ||
| '' | *[!0-9]*) grace=300 ;; | ||
| esac | ||
| age=$((now - FM_CLAIM_CREATED)) | ||
| [ "$age" -ge "$grace" ] |
There was a problem hiding this comment.
Active spawns lose claims
Spawn acquires a claim before setup but publishes the task record much later. If setup takes longer than the 300-second grace period, this check treats the missing record as proof that the claim is stale, even though spawn is still running. Another home can take the claim, leaving both homes free to dispatch against the same target.
| if [ -n "$HARNESS" ]; then | ||
| LANE_CAP_MODEL=$MODEL | ||
| LANE_CAP_EXCLUDE= | ||
| if [ "$RELAUNCH" -eq 1 ]; then | ||
| LANE_CAP_EXCLUDE=$ID | ||
| [ -n "$LANE_CAP_MODEL" ] || LANE_CAP_MODEL=$(fm_meta_get "$RELAUNCH_META" model) | ||
| fi | ||
| fm_provider_cap_refuse "$STATE" "$CONFIG" "$HARNESS" "$LANE_CAP_MODEL" "$LANE_CAP_EXCLUDE" || exit 1 | ||
| fi | ||
| if [ "$HARNESS" = openhands ]; then | ||
| if [ -z "$MODEL" ] || [ "$MODEL" = default ]; then | ||
| if [ -n "${LLM_MODEL:-}" ]; then | ||
| MODEL=$LLM_MODEL |
There was a problem hiding this comment.
OpenHands checks wrong provider cap
When an OpenHands spawn gets its model from LLM_MODEL rather than --model, the cap check runs before that model is assigned. Admission checks the openhands bucket, but the task is later recorded and counted under the model's provider. These spawns can therefore exceed that provider's configured cap.
| if [ "$EXAMINED" -ge "$MAX_HOLDS" ] || budget_exhausted; then | ||
| DEFERRED=$((DEFERRED + 1)) | ||
| continue |
There was a problem hiding this comment.
Deferred holds never get checked
Every sweep sorts aged holds oldest-first and checks only the first MAX_HOLDS, without saving a position for the next sweep. If more than the default 12 aged holds remain, each sweep checks the same 12 and defers the rest indefinitely. Those holds never receive a verdict in the docket.
| { | ||
| printf 'LLM_API_KEY=%s\n' "$(shell_quote "$OPENHANDS_API_KEY")" | ||
| printf 'LLM_MODEL=%s\n' "$(shell_quote "$MODEL")" | ||
| } > "$OPENHANDS_ENV_FILE" || { |
There was a problem hiding this comment.
Failed spawn retains API key
OpenHands writes LLM_API_KEY to a per-task env file before checking whether the pane starts processing its brief. If that readiness check fails, spawn closes the pane and exits without removing the file. The credential remains in state/ until a separate teardown, so abort cleanup should remove it.
How this was verified: The readiness-failure path reaches endpoint and abort cleanup, neither of which removes the secret-bearing env file.
Intent
Clean up the firstmate herdr instance, which has become useless for getting work done. Review https://github.com/kunchenguid/kun and https://github.com/kunchenguid/firstmate, debug the current system, and fix the issues so it is back to being a fast agentic software factory that can be used and that does not crash and have issues all the time.
Context from the diagnosis: this home runs a private fork (keenvc/firstmate, main at 9928ab1) that is 164 commits behind upstream kunchenguid/firstmate main and carries about 30 fork-only commits. Upstream has since fixed watcher stalls and re-arm failures (#6103, #5941, #5594, #5362, #5732), reclaim of tasks whose herdr endpoint was destroyed (#5007), teardown retiring watcher markers, orphan journals and wake rows (#5997, #5390), session-start fixes (#6125), and bounded status classification (#5383). Session start currently exceeds its 120s bound, 47 of 99 task records read as "unreadable runs table", and a straight merge of upstream into the fork reports 32 conflicts.
What Changed
kunchenguid/firstmatemain into the fork. This brings in the watcher stall and re-arm fixes inbin/fm-watch.sh, and reclaim of tasks whose herdr endpoint was destroyed. It also brings teardown cleanup of watcher markers, orphan journals and wake rows inbin/fm-teardown.sh. New upstream scripts arrive with it:fm-claim.sh,fm-claim-lib.sh,fm-hold-reverify.sh,fm-provider-lib.shandfm-provider-load.sh. Cline and OpenHands harness adapters, docs and verification notes are added, andfm-spawn.sh,fm-busy-lib.sh,fm-composer-lib.shandfm-fleet-snapshot.share extended.bin/backends/herdr.shandbin/fm-backend.shwith a timeout cap, and adjustedbin/fm-session-start.shandbin/fm-crew-state.sh, so session start and status classification no longer hang past their bounds. The fleet-snapshot ARG_MAX file transport is restored after the merge. The timeout cap is documented indocs/herdr-backend.mdanddocs/configuration.md..gitignoreand CI workflow are also updated.Risk Assessment
✅ Low: The only change since the prior review is a one-line removal of a duplicate
.omc/entry from .gitignore, and.omc/is still ignored once.Testing
I ran the session-start, herdr probe-timeout, teardown, watcher-arm and herdr-lab test files. Two real-Herdr e2e tests and a live lab provision/run/teardown also passed. The session-start, probe-timeout, teardown and watcher-arm checks used fakes or fixtures, so they are recorded as untested against the live product. I did not launch a real primary or spawn a lane in the lab, so the intent's "session start under 120s" and "unreadable runs table" symptoms were not measured against real fleet state. Watch triage was not re-run. Running
bin/fm-herdr-lab.sh preparefirst made the followingprovisionrefuse an existing tripwire, so I ranprovisionalone.Evidence: herdr lab live transcript
Source: herdr lab live transcript
Evidence: session-start test log
Source: session-start test log
Evidence: herdr probe-timeout test log
Source: herdr probe-timeout test log
Evidence: teardown test log
Source: teardown test log
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
.agents/skills/agent-skill-trigger-index/SKILL.md- branch carries 45 commit(s) that exist on your local main branch but were never pushed to origin/main; these may be unintended bundled work (proposed PR changes 79 file(s)):Confirm these commits belong in this PR before approving, or manually separate the intended work onto origin/main before gating.
🔧 **Review** - 1 issue found → auto-fixed ✅
.gitignore:8- .gitignore now lists.omc/twice (lines 6 and 8). The second entry is a redundant leftover from the merge and changes nothing; remove one.🔧 Fix applied.
✅ Re-checked - no issues remain.
🔧 **Test** - 3 issues found → auto-fixed ✅
bin/fm-session-start.sh- Merging the fork's bounded-herdr-CLI probes (fix(bin): bound herdr CLI probes so a hung read cannot wedge a supervisor #4988) with the session-start endpoint bound broketests/fm-session-start.test.sh. The backend CLI runs in its own process group under a 10s bound, so when session start's per-read bound (for example 2s) killed its own group, a hung herdr survived up to 10s. The test's fake herdr also killed the wrong ancestor because the bounded wrapper added extra process hops. Fixed both:bin/fm-session-start.shnow capsFM_BACKEND_HERDR_CLI_TIMEOUTat the per-read bound, and the deadly-read fake intests/fm-session-start.test.shwalks ancestry to the read's own shell. The full session-start file now passes.tests/fm-watch-triage.test.sh-tests/fm-watch-triage.test.shran 101 passing assertions with no failures but did not finish inside the 900s cap. I could not confirm full completion.bin/fm-herdr-lab.shwith a named fm-lab-* session on a host where herdr can be stood up.bash tests/fm-session-start.test.sh(failed on the merge, passes after the fix; the hung-read and padded-zero-bound tests passed in isolation)bash tests/fm-teardown.test.shbash tests/fm-teardown-endpoint-safety.test.shbash tests/fm-backend-herdr-probe-timeout.test.shbash tests/fm-watch-arm.test.shbash tests/fm-watch-triage.test.sh(101 ok, no failures, hit the 900s cap)checked the base commit a774c448 in a temporary worktree: session-start had no failures there, so the failure came from this change🔧 Fix applied.
✅ Re-checked - no issues remain.
tests/fm-session-start.test.sh(exit 0)tests/fm-backend-herdr-probe-timeout.test.sh(exit 0)tests/fm-teardown-endpoint-safety.test.sh(exit 0)tests/fm-teardown.test.sh(107 ok, exit 0)tests/fm-watch-arm.test.sh(exit 0)tests/fm-herdr-lab.test.sh(exit 0)tests/fm-herdr-session-cleanup-e2e.test.sh(real Herdr, exit 0)tests/fm-backend-herdr-respawn-idem-e2e.test.sh(real Herdr, exit 0)bin/fm-herdr-lab.sh provision,run workspace list, thenteardownon a fm-lab-gate-* session (all exit 0)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.