diff --git a/AGENTS.md b/AGENTS.md index a8f8ff629a3..6997415bafc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,7 +104,8 @@ backups/ root-local preservation files; not a canonical tracked surf .status appended by crewmates: ": " wake-event lines, not current-state truth .turn-ended touched by turn-end hooks .grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown - .meta written by fm-spawn: window=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; non-default backends also record backend=, and Herdr records herdr_session=, herdr_workspace_id=, herdr_tab_id=, and herdr_pane_id=; kind=secondmate also records home= and projects= (fm-pr-check appends pr= and GitHub's pr_head= when available; fm-x-link appends x_request= and x_request_ts= for an X-mention-originated task, section 14) + .meta written by fm-spawn: window=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; non-default backends also record backend=, with Herdr-specific label and exact-id fields owned by docs/herdr-backend.md; kind=secondmate also records home= and projects= (fm-pr-check appends pr= and GitHub's pr_head= when available; fm-x-link appends x_request= and x_request_ts= for an X-mention-originated task, section 14) + .herdr-label private pre-create recovery journal for a Herdr tab; removed after complete task metadata is published (field and recovery contract: docs/herdr-backend.md) .check.sh optional slow poll you write per task (e.g. merged-PR check) x-watch.check.sh generated X-mode relay poll shim; present only when opted in (section 14) x-inbox/ generated X-mode pending mention payloads; fmx-respond drains it (section 14) @@ -122,9 +123,9 @@ backups/ root-local preservation files; not a canonical tracked surf ``` Task ids are short kebab slugs with a random suffix, e.g. `fix-login-k3`. -The tmux window for a tmux-backed task is named `fm-`. +The tmux window for a tmux-backed task is named `fm-`; Herdr display labels do not change that naming. `fm-spawn.sh` creates that window by tmux window ID, disables automatic and application-driven renaming, restores the canonical name, and verifies it before sending any pane commands. -Herdr-backed tasks instead use a `fm-` tab and record an opaque `session:pane` target; the Herdr workspace is scoped to the firstmate home. +Herdr-backed tasks instead use one ` - · ` display tab, with kind shown as `Crew`, `Scout`, or `2nd`, set once at spawn. The full task id plus exact Herdr session, workspace, tab, and pane ids remain machine identity; the Herdr workspace is scoped to the firstmate home, and legacy `fm-` tabs remain discoverable for recovery. After creation, tmux targets the immutable window ID rather than the mutable `session:window-name` label; Herdr targets the recorded pane ID. If setup cannot prove the backend endpoint, it cleans up the uniquely identified new endpoint and aborts. ## 3. Bootstrap (run at every session start) diff --git a/README.md b/README.md index c7d8035eecf..4cf4f3d79b1 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Outside tmux, tmux-backed crewmates land in a detached `firstmate` session you c │ backend sends / status files │ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ - │fm-task1│ │fm-task2│ ... │fm-taskN│ tmux windows by default; Herdr tabs when selected + │ task 1 │ │ task 2 │ ... │ task N │ fm- tmux windows; readable Herdr tabs when selected │crewmate│ │crewmate│ │crewmate│ one autonomous agent each └───┬────┘ └───┬────┘ └───┬────┘ ▼ ▼ ▼ diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 73c84605994..aaf10b6a502 100755 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -9,8 +9,10 @@ # # One Herdr workspace is kept per firstmate home: `firstmate` for the primary # and `2ndmate-` for a seeded secondmate home. Each task is one tab with a -# single root pane. Targets are `:`; pane ids contain a colon, -# so parsing always splits on the first colon only. +# single root pane. Visible task labels are presentation only; full task ids and +# exact response-derived ids remain machine identity. Targets are +# `:`; pane ids contain a colon, so parsing always splits on +# the first colon only. FM_BACKEND_HERDR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" FM_ROOT="${FM_ROOT_OVERRIDE:-${FM_ROOT:-$FM_BACKEND_HERDR_ROOT}}" @@ -28,6 +30,8 @@ FM_BACKEND_HERDR_LOCK_WAIT_ATTEMPTS=${FM_BACKEND_HERDR_LOCK_WAIT_ATTEMPTS:-100} . "$FM_BACKEND_HERDR_ROOT/bin/fm-transition-lib.sh" # shellcheck source=bin/fm-composer-lib.sh . "$FM_BACKEND_HERDR_ROOT/bin/fm-composer-lib.sh" +# shellcheck source=bin/fm-task-label-lib.sh +. "$FM_BACKEND_HERDR_ROOT/bin/fm-task-label-lib.sh" fm_backend_herdr_workspace_label() { local marker="$FM_HOME/$FM_BACKEND_HERDR_SECONDMATE_MARKER" id @@ -125,6 +129,10 @@ fm_backend_herdr_workspace_lock_path() { printf '%s/.fm-herdr-workspace.lock' "$FM_HOME" } +fm_backend_herdr_label_lock_path() { + printf '%s/.fm-herdr-label.lock' "$1" +} + fm_backend_herdr_pid_start() { local pid=$1 proc_stat out local -a proc_fields @@ -191,12 +199,15 @@ fm_backend_herdr_lock_owner_status() { } fm_backend_herdr_lock_discard() { - local lock=$1 owner + local lock=$1 owner owner_dir owner_name + owner_dir=$(dirname "$lock") if [ -L "$lock" ]; then owner=$(readlink "$lock" 2>/dev/null || true) rm -f "$lock" - case "$owner" in - "$FM_HOME"/.fm-herdr-workspace.owner.*) rm -f "$owner" ;; + owner_name=$(basename "$owner") + case "$owner" in "$owner_dir"/*) + case "$owner_name" in .fm-herdr-*.owner.*) rm -f "$owner" ;; esac + ;; esac elif [ -d "$lock" ]; then rm -f "$lock/pid" "$lock/pid-start" @@ -207,9 +218,11 @@ fm_backend_herdr_lock_discard() { } fm_backend_herdr_workspace_lock_acquire() { - local lock=$1 attempt=0 stale_status quarantine pid start owner acquired=0 - [ -d "$FM_HOME" ] || return 1 - owner=$(mktemp "$FM_HOME/.fm-herdr-workspace.owner.XXXXXX" 2>/dev/null) || return 1 + local lock=$1 attempt=0 stale_status quarantine pid start owner acquired=0 lock_dir lock_name + lock_dir=$(dirname "$lock") + lock_name=$(basename "$lock") + [ -d "$lock_dir" ] || return 1 + owner=$(mktemp "$lock_dir/$lock_name.owner.XXXXXX" 2>/dev/null) || return 1 pid=${BASHPID:-$$} start=$(fm_backend_herdr_pid_start "$pid") || { rm -f "$owner"; return 1; } printf '%s\n%s\n' "$pid" "$start" > "$owner" || { rm -f "$owner"; return 1; } @@ -302,6 +315,18 @@ fm_backend_herdr_workspace_find() { # ' 2>/dev/null } +fm_backend_herdr_workspace_tab_labels() { # [workspace] + local session=$1 wsid=${2:-} tabs + [ -n "$wsid" ] || wsid=$(fm_backend_herdr_workspace_find "$session") || return 1 + [ -n "$wsid" ] || return 0 + tabs=$(fm_backend_herdr_cli "$session" tab list --workspace "$wsid" 2>/dev/null) || return 1 + printf '%s' "$tabs" | jq -r ' + if (.result.tabs | type) == "array" + then .result.tabs[] | select((.label | type) == "string") | .label + else error("missing result.tabs") + end' 2>/dev/null +} + fm_backend_herdr_workspace_ids() { # local session=$1 out out=$(fm_backend_herdr_cli "$session" workspace list 2>/dev/null) || return 1 @@ -565,12 +590,10 @@ fm_backend_herdr_agent_alive() { # -> alive|dead|unknown esac } -fm_backend_herdr_create_task() { #