From e6ff0697b73db06d7dd29f2d293d769c528246df Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 19:14:03 -0700 Subject: [PATCH 1/5] feat(herdr): apply mate naming convention to workspace/tab labels Mates (main firstmate and secondmates) now get uppercase - herdr workspace and tab labels (1M-FIRSTMATE, 2M-), visually distinct from lowercase fm- crewmate tabs, so the captain can tell supervisors from workers in herdr's sidebar at a glance. - bin/backends/herdr.sh: add fm_backend_herdr_mate_scope (uppercase, sanitize, UNKNOWN fallback) and rewrite fm_backend_herdr_workspace_label to use it; extend is_top_level_parent to recognize the new format while keeping the legacy firstmate/2ndmate- matcher frozen. - bin/fm-spawn.sh: label a secondmate's own live-agent tab with its mate label instead of the ordinary fm- task label. - docs/herdr-backend.md, docs/configuration.md: document the convention and the no-migration caveat for pre-existing workspaces. - tests: update herdr fixtures and fakebins to the new label format across fm-backend-herdr.test.sh, the launcher-workspace/smoke/ workspace-per-home/presentation e2e suites, and fix a genuine regression in fm-session-start.test.sh's fake herdr secondmate recovery fixture that still emitted the old label format. --- bin/backends/herdr.sh | 44 +++-- bin/fm-spawn.sh | 14 +- docs/configuration.md | 4 +- docs/herdr-backend.md | 17 +- ...ckend-herdr-launcher-workspace-e2e.test.sh | 40 ++--- .../fm-backend-herdr-presentation-e2e.test.sh | 56 +++---- tests/fm-backend-herdr-smoke.test.sh | 12 +- ...ckend-herdr-workspace-per-home-e2e.test.sh | 10 +- tests/fm-backend-herdr.test.sh | 155 ++++++++++++------ tests/fm-session-start.test.sh | 3 +- 10 files changed, 228 insertions(+), 127 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index f46148fcb1..9ec9729a32 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -56,8 +56,8 @@ # never overrides a real invocation. It exists only so this file's own unit # tests, which source it directly without that preamble, resolve to a sane # default (the firstmate repo root - never a secondmate home, so -# fm_backend_herdr_workspace_label falls through to "firstmate" exactly like -# pre-P3 behavior when a test does not care about home-specific labeling). +# fm_backend_herdr_workspace_label falls through to "1M-FIRSTMATE" when a +# test does not care about home-specific labeling). FM_BACKEND_HERDR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" FM_ROOT="${FM_ROOT_OVERRIDE:-${FM_ROOT:-$FM_BACKEND_HERDR_ROOT}}" FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" @@ -110,11 +110,26 @@ FM_BACKEND_HERDR_SECONDMATE_MARKER=".fm-secondmate-home" # No send, capture, Treehouse, or general task-ownership path reads it. FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX=".herdr-presentation" +# fm_backend_herdr_mate_scope: derive the naming-convention "scope" tag from a +# secondmate id (docs/herdr-backend.md "Mate naming convention"). Pure and +# independently unit-testable: uppercases, then replaces every character +# outside A-Z0-9 with "-", squeezes repeats, and trims leading/trailing "-". +# An id that sanitizes to nothing (empty input, or no surviving alphanumeric) +# falls back to the literal "UNKNOWN" so a malformed or missing marker can +# never produce an empty or colliding workspace label. +fm_backend_herdr_mate_scope() { + local id=$1 scope + scope=$(printf '%s' "$id" | tr '[:lower:]' '[:upper:]' | LC_ALL=C tr -c 'A-Z0-9' '-' | tr -s '-') + scope=${scope#-} + scope=${scope%-} + printf '%s' "${scope:-UNKNOWN}" +} + # fm_backend_herdr_workspace_label: the per-firstmate-HOME herdr workspace -# label (docs/herdr-backend.md "Default task container shape"). The PRIMARY home (no -# secondmate marker) resolves to the constant "firstmate", byte-identical to -# every pre-existing task's recorded label - no forced migration. A SECONDMATE -# home resolves to "2ndmate-", so its tasks land in their own +# label (docs/herdr-backend.md "Mate naming convention"), always uppercase +# "-". The PRIMARY home (no secondmate marker) resolves to +# the constant "1M-FIRSTMATE". A SECONDMATE home resolves to +# "2M-", so its tasks land in their own # workspace, obviously distinguishable from the primary's (and from every # other secondmate's) in herdr's spaces sidebar. Read fresh from FM_HOME on # every call rather than cached at source time: FM_HOME is the home's own @@ -127,12 +142,10 @@ fm_backend_herdr_workspace_label() { local marker="$FM_HOME/$FM_BACKEND_HERDR_SECONDMATE_MARKER" id if [ -f "$marker" ]; then id=$(tr -d '[:space:]' < "$marker" 2>/dev/null) - if [ -n "$id" ]; then - printf '2ndmate-%s' "$id" - return 0 - fi + printf '2M-%s' "$(fm_backend_herdr_mate_scope "$id")" + return 0 fi - printf 'firstmate' + printf '1M-FIRSTMATE' } # fm_backend_herdr_cli: run `herdr ` scoped to , setting @@ -641,7 +654,10 @@ fm_backend_herdr_projection_close_pane_focus_preserving() { # is the owning FM_HOME label (firstmate or 2ndmate-). +# is the owning FM_HOME label (docs/herdr-backend.md "Mate +# naming convention": 1M-FIRSTMATE or 2M-, or - read-only, for +# already-adjacent pre-existing workspaces - the legacy firstmate/2ndmate- +# form). # Optional is that parent's EXACT id, which the caller # already resolved from the launching agent's own herdr identity. When given it # anchors the owning parent by id, so two workspaces sharing the home label no @@ -681,7 +697,9 @@ fm_backend_herdr_projection_order_best_effort() { # , + # identical to every other backend), except a --secondmate spawn's own tab + # IS that mate's live agent, so it gets the mate naming convention's + # uppercase "-" label (docs/herdr-backend.md "Mate naming + # convention") instead of a lowercase task-style name. + HERDR_TASK_LABEL=$W if [ "$KIND" = secondmate ]; then HERDR_LABEL_HOME=$PROJ_ABS HERDR_LAUNCHER_RELATIONSHIP=other-home + HERDR_TASK_LABEL=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_workspace_label) fi HERDR_PRESENTATION_JOURNAL=$(fm_backend_herdr_projection_journal_path "$STATE" "$ID") HERDR_PROJECTED=0 @@ -1172,13 +1180,13 @@ case "$BACKEND" in HERDR_SEEDED_DEFAULT_TAB_ID=${HERDR_CONTAINER_RAW#*$'\t'} HERDR_SES=${CONTAINER%%:*} HERDR_WORKSPACE_ID=${CONTAINER#*:} - HERDR_TASK_IDS=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_create_task "$CONTAINER" "$W" "$PROJ_ABS" "$HERDR_SEEDED_DEFAULT_TAB_ID") || exit 1 + HERDR_TASK_IDS=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_create_task "$CONTAINER" "$HERDR_TASK_LABEL" "$PROJ_ABS" "$HERDR_SEEDED_DEFAULT_TAB_ID") || exit 1 read -r HERDR_TAB_ID HERDR_PANE_ID <&2 + echo "error: herdr did not return a tab/pane id for $HERDR_TASK_LABEL" >&2 exit 1 fi T="$HERDR_SES:$HERDR_PANE_ID" diff --git a/docs/configuration.md b/docs/configuration.md index 4469930014..c0cb1622a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -100,8 +100,8 @@ These five sentences are the single owner of the task-selector vocabulary; backe `fm-teardown.sh ` takes a task id directly and validates the complete metadata-only endpoint identity before any runtime dispatch or cleanup mutation. Missing, empty, duplicate, malformed, backend-inconsistent, or task-mismatched endpoint records are preserved and refused. Legacy tmux metadata remains cleanup-compatible when its exact window name is `fm-`; opaque non-tmux endpoints require their recorded `endpoint_task_id=` binding, except legacy Herdr metadata lacking that binding, which self-repairs by appending it once the live pane is confirmed to still belong to the task, and otherwise refuses without mutation (see [herdr-backend.md](herdr-backend.md#endpoint-metadata)). -`FM_HOME` determines Herdr's home label: the primary home uses `firstmate`, and a secondmate home marked by `.fm-secondmate-home` uses `2ndmate-`. -[`herdr-backend.md`](herdr-backend.md#watching-and-task-containers) owns launcher-bound workspace placement, the label-only fallback, collision handling, and recovery behavior. +`FM_HOME` determines Herdr's home label: the primary home uses `1M-FIRSTMATE`, and a secondmate home marked by `.fm-secondmate-home` uses `2M-`, uppercase and derived from its marker id. +[`herdr-backend.md`](herdr-backend.md#watching-and-task-containers) owns launcher-bound workspace placement, the label-only fallback, collision handling, and recovery behavior, and [Mate naming convention](herdr-backend.md#mate-naming-convention) owns the exact rank/scope derivation. The optional local `config/herdr-presentation-spaces` presence flag instead enables Herdr's default-off disposable single-task visual projection; [Optional presentation spaces](herdr-backend.md#optional-presentation-spaces) owns its behavior, safety limits, recovery contract, and narrow locked session-start cleanup of exact restored idle-shell children. The flag is default-off and inherited into secondmate homes under the primary-authoritative contract owned by [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md). For normal herdr operations, `HERDR_SESSION` selects the named session, but destructive test cleanup must not rely on `HERDR_SESSION` alone. diff --git a/docs/herdr-backend.md b/docs/herdr-backend.md index f75cdb69f0..bbbbd1d52e 100644 --- a/docs/herdr-backend.md +++ b/docs/herdr-backend.md @@ -35,9 +35,9 @@ Real harness credential tests remain opt-in rather than part of default CI. The ordinary topology puts one task tab per endpoint in the exact workspace of the Firstmate or secondmate that launches it. When the launcher has no Herdr workspace to inherit, the adapter maintains one durable home-labeled workspace instead. -The primary home label is `firstmate`. -A secondmate home label is `2ndmate-`, derived from its validated `.fm-secondmate-home` marker. -A secondmate launched by the primary receives a narrowly scoped home override during container creation. +The primary home label is `1M-FIRSTMATE`. +A secondmate home label is `2M-`, uppercase and derived from its validated `.fm-secondmate-home` marker id; see "Mate naming convention" below for the full contract. +A secondmate launched by the primary receives a narrowly scoped home override during container creation, and its own live-agent tab (not only its workspace) carries the same uppercase mate label. Attach to the selected named Herdr session and switch to the relevant home workspace to watch its task tabs. Routine supervision uses `bin/fm-peek.sh ` and `FM_HOME= bin/fm-send.sh ''` without attaching. @@ -57,14 +57,23 @@ That covers a missing or unusable socket identity, a closed or unreadable launch Firstmate running outside Herdr entirely has no launcher workspace to inherit, so its workers use this home's own labeled workspace, created on first use. That path needs the home label to identify exactly one workspace: two workspaces sharing it are an unresolvable placement and refuse rather than adopting either. -Avoid naming a personal workspace `firstmate` or `2ndmate-` for that reason, and because the adapter cannot distinguish that label collision from its own container. +Avoid naming a personal workspace `1M-FIRSTMATE` or `2M-` for that reason, and because the adapter cannot distinguish that label collision from its own container. An older secondmate workspace using `firstmate-` is not migrated automatically; rename it manually before expecting new tasks or recovery to use it. +An installation upgrading from the pre-mate-naming-convention `firstmate`/`2ndmate-` labels is not migrated either: the first spawn into an already-running home mints a fresh `1M-FIRSTMATE`/`2M-` workspace rather than adopting the old one, leaving the old workspace behind to close or merge manually. Recovery and list-live still scan the first workspace matching the home label, because they address panes they already recorded rather than choosing where new work goes. Existing task operations use recorded endpoint ids and do not move a live task when labels change. The per-home workspace is reused while it has task tabs. Closing its last tab can remove the workspace, and the next spawn recreates it. +## Mate naming convention + +A mate's workspace and its own live-agent tab are always labeled uppercase `-`, so the captain can tell a supervisor from a worker in Herdr's sidebar at a glance. +`materank` is `1M` for the main firstmate, `2M` for a secondmate, `3M` for a third mate if one ever exists. +`scope` is the mate's own registered id, uppercased: the primary defaults to the literal scope `FIRSTMATE` (giving `1M-FIRSTMATE`), and a secondmate's scope comes from its `.fm-secondmate-home` marker id (`bin/backends/herdr.sh`'s `fm_backend_herdr_mate_scope`), for example `2M-BEADME`. +That helper sanitizes the id (uppercase, non-alphanumeric characters collapsed to a single `-`, leading/trailing `-` trimmed) and falls back to the literal scope `UNKNOWN` when the id is empty or sanitizes to nothing, so a malformed or missing marker never produces an empty or colliding label. +A crewmate spawned by any mate inherits that mate's placement but is never uppercase: its tab keeps the ordinary lowercase `fm-` task label, so a mate and its subordinate crewmates are always visually distinct and never collide. + ## Optional presentation spaces Create local gitignored `config/herdr-presentation-spaces` to request a disposable one-task workspace for each new crewmate or scout. diff --git a/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh b/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh index ca5cc4575e..a08da386bb 100755 --- a/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +++ b/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh @@ -7,7 +7,7 @@ # workspace of the firstmate or secondmate process that launched it, identified # from that process's own Herdr pane rather than from a workspace label. Herdr # enforces no workspace-label uniqueness, so two workspaces can both be labeled -# "firstmate", and the previous label-first-match resolution put the worker in +# "1M-FIRSTMATE", and the previous label-first-match resolution put the worker in # whichever one sorted first - visibly the wrong space whenever the launcher was # not in it. # @@ -156,6 +156,7 @@ LAB_SOCKET=$(lab session list --json 2>/dev/null \ PRIMARY_HOME="$TMP_ROOT/primary-home" mkdir -p "$PRIMARY_HOME/state" "$PRIMARY_HOME/config" SM_ID="lwsm1" +SM_LABEL="2M-LWSM1" SM_HOME="$TMP_ROOT/secondmate-home" mkdir -p "$SM_HOME/state" "$SM_HOME/config" "$SM_HOME/projects" "$SM_HOME/bin" "$SM_HOME/data" printf '# scratch secondmate home AGENTS.md placeholder\n' > "$SM_HOME/AGENTS.md" @@ -163,6 +164,7 @@ printf '%s\n' "$SM_ID" > "$SM_HOME/.fm-secondmate-home" printf 'trivial e2e secondmate charter: nothing to do.\n' > "$SM_HOME/data/charter.md" SM2_ID="lwsm2" +SM2_LABEL="2M-LWSM2" SM2_HOME="$TMP_ROOT/secondmate-home-2" mkdir -p "$SM2_HOME/state" "$SM2_HOME/config" "$SM2_HOME/projects" "$SM2_HOME/bin" "$SM2_HOME/data" printf '# scratch secondmate home AGENTS.md placeholder\n' > "$SM2_HOME/AGENTS.md" @@ -209,9 +211,9 @@ UNIQA_PANE=$(grep '^herdr_pane_id=' "$UNIQA_META" | cut -d= -f2-) [ -n "$UNIQA_PANE" ] || fail "uniqA meta is missing herdr_pane_id" WS_PRIMARY=$(workspace_of_pane "$UNIQA_PANE") [ -n "$WS_PRIMARY" ] || fail "could not read uniqA's workspace" -[ "$(label_of_workspace "$WS_PRIMARY")" = firstmate ] || fail "uniqA did not land in a 'firstmate' workspace" +[ "$(label_of_workspace "$WS_PRIMARY")" = 1M-FIRSTMATE ] || fail "uniqA did not land in a '1M-FIRSTMATE' workspace" [ "$(focused_workspace)" = "$WS_OTHER" ] || fail "the spawn stole focus from the captain's workspace" -pass "real herdr E2E: with one 'firstmate' workspace and no herdr parent, a crewmate still lands in this home's own workspace without stealing focus" +pass "real herdr E2E: with one '1M-FIRSTMATE' workspace and no herdr parent, a crewmate still lands in this home's own workspace without stealing focus" # --- 2. unique label, WITH a launcher pane: same workspace, now by identity -- @@ -219,7 +221,7 @@ read -r _ _ LAUNCH_PRIMARY_PANE </dev/null \ | jq -r '["x","x", .result.root_pane.pane_id] | @tsv' | tr '\t' ' ') EOF -[ -n "$LAUNCH_PRIMARY_PANE" ] || fail "could not create a launcher pane inside the 'firstmate' workspace" +[ -n "$LAUNCH_PRIMARY_PANE" ] || fail "could not create a launcher pane inside the '1M-FIRSTMATE' workspace" spawn_from_launcher "$LAUNCH_PRIMARY_PANE" "$PRIMARY_HOME" uniqB "$PROJ" [ "$SPAWN_RC" -eq 0 ] || fail "a primary spawn from a launcher pane failed"$'\n'"$(cat "$SPAWN_ERR")" @@ -227,7 +229,7 @@ UNIQB_META="$PRIMARY_HOME/state/uniqB.meta" record_worktree "$UNIQB_META" UNIQB_PANE=$(grep '^herdr_pane_id=' "$UNIQB_META" | cut -d= -f2-) [ "$(workspace_of_pane "$UNIQB_PANE")" = "$WS_PRIMARY" ] \ - || fail "a crewmate launched from the 'firstmate' workspace must stay in it" + || fail "a crewmate launched from the '1M-FIRSTMATE' workspace must stay in it" pass "real herdr E2E: the normal unique-label path is unchanged when the launcher's own pane identifies the workspace" # --- 2b. presentation spaces ON: the projected child is created and bound @@ -261,12 +263,12 @@ pass "real herdr E2E: presentation spaces still create the isolated child worksp # Herdr pane so the identity comes from Herdr's own injection ---------- read -r WS_PRIMARY_DUP _ LAUNCH_DUP_PANE </dev/null | jq -r '[.result.workspaces[]? | select(.label == "firstmate")] | length') -[ "$DUP_COUNT" = 2 ] || fail "expected exactly two 'firstmate' workspaces, got $DUP_COUNT" +[ -n "$WS_PRIMARY_DUP" ] || fail "could not create the second '1M-FIRSTMATE' workspace" +[ "$WS_PRIMARY_DUP" != "$WS_PRIMARY" ] || fail "the two '1M-FIRSTMATE' workspaces must be distinct" +DUP_COUNT=$(lab workspace list 2>/dev/null | jq -r '[.result.workspaces[]? | select(.label == "1M-FIRSTMATE")] | length') +[ "$DUP_COUNT" = 2 ] || fail "expected exactly two '1M-FIRSTMATE' workspaces, got $DUP_COUNT" WS_PRIMARY_TABS_BEFORE=$(tab_labels_of_workspace "$WS_PRIMARY") cat > "$TMP_ROOT/spawn-in-pane.sh" </dev/null | jq -r '[.result.tabs[]? | select(.label == "fm-dupD")] | length') @@ -371,10 +373,10 @@ pass "real herdr E2E: a launcher pane that no longer exists refuses before any w # --- 6. a secondmate launching its own worker gets the same guarantee ------- read -r WS_SM_DECOY _ _ </dev/null 2>&1; then fi lab pane get "$LAUNCH_DUP_PANE" >/dev/null 2>&1 || fail "teardown closed the launcher's own pane" lab pane get "$UNIQB_PANE" >/dev/null 2>&1 || fail "teardown closed an unrelated worker's pane in the other same-labeled workspace" -[ "$(label_of_workspace "$WS_PRIMARY_DUP")" = firstmate ] || fail "teardown removed or renamed the launcher's workspace" +[ "$(label_of_workspace "$WS_PRIMARY_DUP")" = 1M-FIRSTMATE ] || fail "teardown removed or renamed the launcher's workspace" pass "real herdr E2E: teardown closes only the worker's own pane and leaves the launcher, its workspace, and the same-labeled sibling intact" if ! cleanup_all; then diff --git a/tests/fm-backend-herdr-presentation-e2e.test.sh b/tests/fm-backend-herdr-presentation-e2e.test.sh index 7a4cc5e944..88eaf05d6d 100755 --- a/tests/fm-backend-herdr-presentation-e2e.test.sh +++ b/tests/fm-backend-herdr-presentation-e2e.test.sh @@ -480,14 +480,14 @@ printf 'Projection abort fixture B.\n' > "$HOME_DIR/data/abort-b/brief.md" printf 'Projection lock contention fixture.\n' > "$HOME_DIR/data/lock-contended/brief.md" make_project "$PROJECT_DIR" -# Keep one ordinary primary task live so the durable firstmate workspace is +# Keep one ordinary primary task live so the durable 1M-FIRSTMATE workspace is # first and remains present while disposable workers are projected around it. spawn_task anchor "$HOME_DIR" "$PROJECT_DIR" > "$TMP_ROOT/anchor.out" 2> "$TMP_ROOT/anchor.err" \ || fail "flag-off anchor spawn failed: $(cat "$TMP_ROOT/anchor.err")" ANCHOR_META="$HOME_DIR/state/anchor.meta" remember_meta_worktree "$ANCHOR_META" >/dev/null FIRSTMATE_WSID=$(grep '^herdr_workspace_id=' "$ANCHOR_META" | cut -d= -f2-) -[ -n "$FIRSTMATE_WSID" ] || fail "anchor metadata did not record the firstmate workspace" +[ -n "$FIRSTMATE_WSID" ] || fail "anchor metadata did not record the 1M-FIRSTMATE workspace" # The same task id and project run once with the flag absent and once with it # present, so Treehouse commands and metadata can be compared directly. @@ -511,9 +511,9 @@ pass "real Herdr lab: flag-off spawn retains the Stage 1 Herdr command sequence teardown_task shape "$HOME_DIR" > "$TMP_ROOT/off-teardown.out" 2> "$TMP_ROOT/off-teardown.err" \ || fail "flag-off teardown failed: $(cat "$TMP_ROOT/off-teardown.err")" -SECOND_ONE_OUT=$(lab workspace create --cwd "$PROJECT_DIR" --label 2ndmate-alpha --no-focus) \ +SECOND_ONE_OUT=$(lab workspace create --cwd "$PROJECT_DIR" --label 2M-ALPHA --no-focus) \ || fail "could not create the first secondmate presentation fixture" -SECOND_TWO_OUT=$(lab workspace create --cwd "$PROJECT_DIR" --label 2ndmate-bravo --focus) \ +SECOND_TWO_OUT=$(lab workspace create --cwd "$PROJECT_DIR" --label 2M-BRAVO --focus) \ || fail "could not create the focused secondmate presentation fixture" SECOND_ONE_WSID=$(printf '%s' "$SECOND_ONE_OUT" | jq -r '.result.workspace.workspace_id // empty') SECOND_TWO_WSID=$(printf '%s' "$SECOND_TWO_OUT" | jq -r '.result.workspace.workspace_id // empty') @@ -646,7 +646,7 @@ LOCK_CONTENTION_META="$HOME_DIR/state/lock-contended.meta" remember_meta_worktree "$LOCK_CONTENTION_META" >/dev/null LOCK_CONTENTION_WSID=$(grep '^herdr_workspace_id=' "$LOCK_CONTENTION_META" | cut -d= -f2-) [ "$LOCK_CONTENTION_WSID" = "$FIRSTMATE_WSID" ] \ - || fail "bounded lock contention did not use the ordinary flat firstmate workspace" + || fail "bounded lock contention did not use the ordinary flat 1M-FIRSTMATE workspace" [ ! -e "$HOME_DIR/state/lock-contended.herdr-presentation" ] \ || fail "bounded lock contention published a projection journal" LOCK_CONTENTION_CALLS=$(sed -n "$((LOCK_CONTENTION_START + 1)),\$p" "$HERDR_CALL_LOG") @@ -690,9 +690,9 @@ remember_meta_worktree "$ORDER_B_META" >/dev/null ORDER_LIST=$(lab workspace list) || fail "could not inspect concurrent presentation ordering" CREATED_LABELS=$(projection_labels_from_log "$PROJECTION_ORDER_START") -EXPECTED_LABELS=$(printf 'firstmate\n%s\n%s\n2ndmate-alpha\n2ndmate-bravo' "$PROJECTED_LABEL" "$CREATED_LABELS") +EXPECTED_LABELS=$(printf '1M-FIRSTMATE\n%s\n%s\n2M-ALPHA\n2M-BRAVO' "$PROJECTED_LABEL" "$CREATED_LABELS") ACTUAL_LABELS=$(printf '%s' "$ORDER_LIST" | jq -r '.result.workspaces[].label') -[ "$ACTUAL_LABELS" = "$EXPECTED_LABELS" ] || fail "workspace order was not firstmate, stable primary block, secondmates: $ACTUAL_LABELS" +[ "$ACTUAL_LABELS" = "$EXPECTED_LABELS" ] || fail "workspace order was not 1M-FIRSTMATE, stable primary block, secondmates: $ACTUAL_LABELS" PRIMARY_IDS=$(printf '%s' "$ORDER_LIST" | jq -r ' .result.workspaces[] | select((.label | startswith("└ ")) or (.label | startswith("firstmate/"))) @@ -704,7 +704,7 @@ MOVE_TARGETS=$(cut -f2 "$MOVE_CALL_LOG") MOVE_INDEXES=$(cut -f3 "$MOVE_CALL_LOG") [ "$MOVE_INDEXES" = $'1\n2\n3' ] \ || fail "concurrent primary workers did not append stably to the contiguous block: $MOVE_INDEXES" -SECOND_ORDER_AFTER=$(printf '%s' "$ORDER_LIST" | jq -r '.result.workspaces[] | select(.label | startswith("2ndmate-")) | .workspace_id') +SECOND_ORDER_AFTER=$(printf '%s' "$ORDER_LIST" | jq -r '.result.workspaces[] | select(.label | startswith("2M-")) | .workspace_id') [ "$SECOND_ORDER_AFTER" = "$SECOND_ORDER_BEFORE" ] \ || fail "primary workspace ordering changed secondmate relative order" [ "$(lab workspace get "$SECOND_TWO_WSID" | jq -r '.result.workspace.focused')" = true ] \ @@ -838,11 +838,11 @@ for ROUND in 1 2 3; do assert_focus_is "$CAPTAIN_FOCUS" "focus wave $ROUND concurrent spawns" assert_raw_presentation_mutations_preserved_since "$WAVE_FOCUS_START" "focus wave $ROUND concurrent spawns" WAVE_LABELS=$(projection_labels_from_log "$WAVE_LOG_START") - WAVE_EXPECTED=$(printf 'firstmate\n%s\n2ndmate-alpha\n2ndmate-bravo' "$WAVE_LABELS") - WAVE_ACTUAL=$(lab workspace list | jq -r '.result.workspaces[] | select(.label == "firstmate" or (.label | startswith("└ ")) or (.label | startswith("2ndmate-"))) | .label') + WAVE_EXPECTED=$(printf '1M-FIRSTMATE\n%s\n2M-ALPHA\n2M-BRAVO' "$WAVE_LABELS") + WAVE_ACTUAL=$(lab workspace list | jq -r '.result.workspaces[] | select(.label == "1M-FIRSTMATE" or (.label | startswith("└ ")) or (.label | startswith("2M-"))) | .label') [ "$WAVE_ACTUAL" = "$WAVE_EXPECTED" ] \ || fail "focus wave $ROUND lost stable contiguous ordering: $WAVE_ACTUAL" - WAVE_SECOND_ORDER=$(lab workspace list | jq -r '.result.workspaces[] | select(.label | startswith("2ndmate-")) | .workspace_id') + WAVE_SECOND_ORDER=$(lab workspace list | jq -r '.result.workspaces[] | select(.label | startswith("2M-")) | .workspace_id') [ "$WAVE_SECOND_ORDER" = "$SECOND_ORDER_BEFORE" ] \ || fail "focus wave $ROUND changed secondmate relative order" @@ -854,7 +854,7 @@ for ROUND in 1 2 3; do wait "$WAVE_B_TEARDOWN_PID" || fail "focus wave $ROUND teardown B failed" assert_focus_is "$CAPTAIN_FOCUS" "focus wave $ROUND concurrent teardowns" WAVE_REMAINING=$(lab workspace list | jq -r '.result.workspaces[].label') - [ "$WAVE_REMAINING" = $'firstmate\n2ndmate-alpha\n2ndmate-bravo' ] \ + [ "$WAVE_REMAINING" = $'1M-FIRSTMATE\n2M-ALPHA\n2M-BRAVO' ] \ || fail "focus wave $ROUND cleanup left a projected workspace behind: $WAVE_REMAINING" done pass "real Herdr lab: three repeated concurrent create/order/cleanup waves have zero active workspace or tab drift" @@ -904,7 +904,7 @@ SECOND_META="$HOME_DIR/state/alpha.meta" || fail "secondmate spawn did not record kind=secondmate" SECOND_WSID=$(grep '^herdr_workspace_id=' "$SECOND_META" | cut -d= -f2-) SECOND_LABEL=$(lab workspace get "$SECOND_WSID" | jq -r '.result.workspace.label') -[ "$SECOND_LABEL" = 2ndmate-alpha ] \ +[ "$SECOND_LABEL" = 2M-ALPHA ] \ || fail "secondmate spawn did not use its flat parent workspace: $SECOND_LABEL" [ -z "$(projection_labels_from_log "$SECOND_SPAWN_LOG_START")" ] \ || fail "secondmate spawn created a corner projection workspace" @@ -924,7 +924,7 @@ propagate_inheritable_config "$HOME_DIR/config" "$SECOND_HOME_B/config" \ || fail "primary presentation flag did not reach secondmate B" pass "real Herdr lab: primary presentation opt-in inherits into real secondmate homes" -# Keep the pre-existing 2ndmate-alpha/bravo workspaces as owning parents and captain focus. +# Keep the pre-existing 2M-ALPHA/2M-BRAVO workspaces as owning parents and captain focus. assert_focus_is "$CAPTAIN_FOCUS" "multi-home captain focus" mkdir -p "$SECOND_HOME_A/data/a1" "$SECOND_HOME_A/data/a2" \ @@ -977,17 +977,17 @@ MULTI_LIST=$(lab workspace list) || fail "could not list multi-home topology" MULTI_LABELS=$(printf '%s' "$MULTI_LIST" | jq -r ' .result.workspaces[] | select( - .label == "firstmate" - or .label == "2ndmate-alpha" - or .label == "2ndmate-bravo" + .label == "1M-FIRSTMATE" + or .label == "2M-ALPHA" + or .label == "2M-BRAVO" or (.label | startswith("└ ")) ) | .label ') MULTI_EXPECTED=$(printf '%s\n' \ - firstmate "$P1_LABEL" "$P2_LABEL" \ - 2ndmate-alpha "$A1_LABEL" "$A2_LABEL" \ - 2ndmate-bravo "$B1_LABEL" "$B2_LABEL") + 1M-FIRSTMATE "$P1_LABEL" "$P2_LABEL" \ + 2M-ALPHA "$A1_LABEL" "$A2_LABEL" \ + 2M-BRAVO "$B1_LABEL" "$B2_LABEL") [ "$MULTI_LABELS" = "$MULTI_EXPECTED" ] \ || fail "multi-home topology was not owning-parent grouped: $MULTI_LABELS" pass "real Herdr lab: primary and two secondmate homes each own a top-level contiguous child block" @@ -1014,18 +1014,18 @@ assert_focus_is "$CAPTAIN_FOCUS" "cross-home concurrent wave" assert_raw_presentation_mutations_preserved_since "$WAVE_CROSS_FOCUS" "cross-home concurrent wave" CROSS_LIST=$(lab workspace list) printf '%s' "$CROSS_LIST" | jq -e ' - ([.result.workspaces[].label] | index("firstmate")) as $fm - | ([.result.workspaces[].label] | index("2ndmate-alpha")) as $a - | ([.result.workspaces[].label] | index("2ndmate-bravo")) as $b + ([.result.workspaces[].label] | index("1M-FIRSTMATE")) as $fm + | ([.result.workspaces[].label] | index("2M-ALPHA")) as $a + | ([.result.workspaces[].label] | index("2M-BRAVO")) as $b | $fm != null and $a != null and $b != null and $fm < $a and $a < $b ' >/dev/null 2>&1 || fail "cross-home concurrent wave reordered parents" PCW_LABEL=$(lab workspace get "$(grep '^herdr_workspace_id=' "$HOME_DIR/state/pcw.meta" | cut -d= -f2-)" | jq -r '.result.workspace.label') ACW_LABEL=$(lab workspace get "$(grep '^herdr_workspace_id=' "$SECOND_HOME_A/state/acw.meta" | cut -d= -f2-)" | jq -r '.result.workspace.label') BCW_LABEL=$(lab workspace get "$(grep '^herdr_workspace_id=' "$SECOND_HOME_B/state/bcw.meta" | cut -d= -f2-)" | jq -r '.result.workspace.label') -case "$PCW_LABEL" in $'└ pcw · p:'*|firstmate) ;; *) fail "cross-home primary label wrong: $PCW_LABEL" ;; esac -case "$ACW_LABEL" in $'└ acw · p:'*|2ndmate-alpha) ;; *) fail "cross-home A label wrong: $ACW_LABEL" ;; esac -case "$BCW_LABEL" in $'└ bcw · p:'*|2ndmate-bravo) ;; *) fail "cross-home B label wrong: $BCW_LABEL" ;; esac +case "$PCW_LABEL" in $'└ pcw · p:'*|1M-FIRSTMATE) ;; *) fail "cross-home primary label wrong: $PCW_LABEL" ;; esac +case "$ACW_LABEL" in $'└ acw · p:'*|2M-ALPHA) ;; *) fail "cross-home A label wrong: $ACW_LABEL" ;; esac +case "$BCW_LABEL" in $'└ bcw · p:'*|2M-BRAVO) ;; *) fail "cross-home B label wrong: $BCW_LABEL" ;; esac pass "real Herdr lab: concurrent primary/A/B spawns preserve parent order and exact focus" # Hold the shared session lock from a different home and force flat fallback. @@ -1059,7 +1059,7 @@ grep -F "presentation focus lock unavailable; using the ordinary flat layout wit remember_meta_worktree "$SECOND_HOME_A/state/aflat.meta" >/dev/null AFLAT_WSID=$(grep '^herdr_workspace_id=' "$SECOND_HOME_A/state/aflat.meta" | cut -d= -f2-) AFLAT_LABEL=$(lab workspace get "$AFLAT_WSID" | jq -r '.result.workspace.label') -[ "$AFLAT_LABEL" = 2ndmate-alpha ] \ +[ "$AFLAT_LABEL" = 2M-ALPHA ] \ || fail "cross-home lock contention did not use the ordinary secondmate home workspace: $AFLAT_LABEL" [ ! -e "$SECOND_HOME_A/state/aflat.herdr-presentation" ] \ || fail "cross-home lock contention published a projection journal" @@ -1240,7 +1240,7 @@ LEGACY_OUT=$(lab workspace create --cwd "$PROJECT_DIR" --label "firstmate/legacy || fail "could not seed a legacy old-format presentation space" LEGACY_WSID=$(printf '%s' "$LEGACY_OUT" | jq -r '.result.workspace.workspace_id // empty') [ -n "$LEGACY_WSID" ] || fail "legacy seed returned no workspace id" -FLAT_TAB_OUT=$(lab tab create --workspace "$(lab workspace list | jq -r '.result.workspaces[] | select(.label == "2ndmate-alpha") | .workspace_id' | head -1)" --cwd "$PROJECT_DIR" --label fm-flat-legacy-tab --no-focus) \ +FLAT_TAB_OUT=$(lab tab create --workspace "$(lab workspace list | jq -r '.result.workspaces[] | select(.label == "2M-ALPHA") | .workspace_id' | head -1)" --cwd "$PROJECT_DIR" --label fm-flat-legacy-tab --no-focus) \ || fail "could not seed a flat secondmate child tab" FLAT_TAB_ID=$(printf '%s' "$FLAT_TAB_OUT" | jq -r '.result.tab.tab_id // empty') mkdir -p "$HOME_DIR/data/post-legacy" diff --git a/tests/fm-backend-herdr-smoke.test.sh b/tests/fm-backend-herdr-smoke.test.sh index 98f1db2e97..a0dab11227 100755 --- a/tests/fm-backend-herdr-smoke.test.sh +++ b/tests/fm-backend-herdr-smoke.test.sh @@ -65,7 +65,7 @@ case "$CONTAINER" in *) fail "container_ensure returned an unexpected shape: $CONTAINER" ;; esac [ -n "$SEEDED_TAB_ID" ] || fail "the first container_ensure in a brand-new isolated session must CREATE the workspace and report its seeded default tab id" -pass "real herdr: container_ensure starts the isolated session's server, creates the firstmate workspace ($CONTAINER), and reports its seeded default tab id ($SEEDED_TAB_ID)" +pass "real herdr: container_ensure starts the isolated session's server, creates the 1M-FIRSTMATE workspace ($CONTAINER), and reports its seeded default tab id ($SEEDED_TAB_ID)" # A second container_ensure must reuse (ADOPT) the same workspace (idempotent) # and report an EMPTY seeded tab id - the created-vs-adopted gate that fixes @@ -77,7 +77,7 @@ CONTAINER2=${CONTAINER2_RAW%%$'\t'*} SEEDED_TAB_ID2=${CONTAINER2_RAW#*$'\t'} [ "$CONTAINER2" = "$CONTAINER" ] || fail "container_ensure is not idempotent: '$CONTAINER' vs '$CONTAINER2'" [ -z "$SEEDED_TAB_ID2" ] || fail "an ADOPTED (reused) workspace must report an EMPTY seeded default tab id, got '$SEEDED_TAB_ID2'" -pass "real herdr: container_ensure is idempotent (reuses/adopts the existing firstmate workspace, reports no seeded default tab on adoption)" +pass "real herdr: container_ensure is idempotent (reuses/adopts the existing 1M-FIRSTMATE workspace, reports no seeded default tab on adoption)" # --- create_task + duplicate refusal + default-tab prune --------------------- @@ -195,8 +195,8 @@ pass "real herdr: a secondmate-shaped home (.fm-secondmate-home) gets its OWN he SM_WSID=${SM_CONTAINER#*:} SM_LABEL_REAL=$(herdr workspace list --session "$SESSION" 2>&1 | jq -r --arg id "$SM_WSID" '.result.workspaces[]? | select(.workspace_id == $id) | .label') -[ "$SM_LABEL_REAL" = "2ndmate-smoketest-sm1" ] || fail "the secondmate workspace's real herdr label should be 2ndmate-smoketest-sm1, got '$SM_LABEL_REAL'" -pass "real herdr: the secondmate-shaped home's workspace is labeled 2ndmate- in herdr itself" +[ "$SM_LABEL_REAL" = "2M-SMOKETEST-SM1" ] || fail "the secondmate workspace's real herdr label should be 2M-SMOKETEST-SM1, got '$SM_LABEL_REAL'" +pass "real herdr: the secondmate-shaped home's workspace is labeled 2M- in herdr itself" SM_TASK_LABEL="fm-smtask1" SM_TASK_IDS=$(FM_HOME="$SM_HOME" fm_backend_herdr_create_task "$SM_CONTAINER" "$SM_TASK_LABEL" /tmp "$SM_SEEDED_TAB_ID") || fail "secondmate create_task failed" @@ -235,8 +235,8 @@ sleep 0.5 fm_backend_herdr_server_ensure "$SESSION" || fail "the isolated session's server did not come back up after the stop" POST_LIST=$(herdr workspace list --session "$SESSION" 2>&1) -POST_PRIMARY_ID=$(printf '%s' "$POST_LIST" | jq -r '.result.workspaces[]? | select(.label == "firstmate") | .workspace_id') -POST_SM_ID=$(printf '%s' "$POST_LIST" | jq -r --arg l "2ndmate-smoketest-sm1" '.result.workspaces[]? | select(.label == $l) | .workspace_id') +POST_PRIMARY_ID=$(printf '%s' "$POST_LIST" | jq -r '.result.workspaces[]? | select(.label == "1M-FIRSTMATE") | .workspace_id') +POST_SM_ID=$(printf '%s' "$POST_LIST" | jq -r --arg l "2M-SMOKETEST-SM1" '.result.workspaces[]? | select(.label == $l) | .workspace_id') [ "$POST_PRIMARY_ID" = "${CONTAINER#*:}" ] || fail "the primary workspace id did not survive the restart: before=${CONTAINER#*:} after=$POST_PRIMARY_ID" [ "$POST_SM_ID" = "$SM_WSID" ] || fail "the secondmate workspace id did not survive the restart: before=$SM_WSID after=$POST_SM_ID" diff --git a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh index 110017e9b8..e76ff528d3 100755 --- a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +++ b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh @@ -19,7 +19,7 @@ # # Covers, at minimum (per the task brief): # - a primary-shaped home (no .fm-secondmate-home marker) spawning a -# crewmate into the "firstmate" workspace +# crewmate into the "1M-FIRSTMATE" workspace # - a secondmate-shaped home (with .fm-secondmate-home) getting its own # labeled workspace when the PRIMARY spawns it (fm-spawn.sh's FM_HOME # shadow for --secondmate) @@ -107,7 +107,7 @@ make_scratch_project() { # PROJ1="$TMP_ROOT/scratch-project-1"; make_scratch_project "$PROJ1" PROJ2="$TMP_ROOT/scratch-project-2"; make_scratch_project "$PROJ2" -# --- 1. primary-shaped home: a crewmate spawns into the "firstmate" space --- +# --- 1. primary-shaped home: a crewmate spawns into the "1M-FIRSTMATE" space --- CM1_OUT="$TMP_ROOT/cm1.out"; CM1_ERR="$TMP_ROOT/cm1.err" FM_SPAWN_NO_GUARD=1 FM_HOME="$PRIMARY_HOME" FM_ROOT_OVERRIDE="$ROOT" \ @@ -131,8 +131,8 @@ assert_contains_local "$CM1_CAPTURE" "primary-crew-ok" "cm1's raw launch command CM1_WSID=$(herdr pane get "$CM1_PANE" --session "$SESSION" 2>/dev/null | jq -r '.result.pane.workspace_id // empty') [ -n "$CM1_WSID" ] || fail "could not read cm1's pane workspace_id" CM1_WS_LABEL=$(herdr workspace list --session "$SESSION" 2>&1 | jq -r --arg id "$CM1_WSID" '.result.workspaces[]? | select(.workspace_id == $id) | .label') -[ "$CM1_WS_LABEL" = "firstmate" ] || fail "a primary-shaped home's crewmate should land in the 'firstmate' workspace, got '$CM1_WS_LABEL'" -pass "real herdr E2E: the primary-shaped home's crewmate landed in the 'firstmate' workspace" +[ "$CM1_WS_LABEL" = "1M-FIRSTMATE" ] || fail "a primary-shaped home's crewmate should land in the '1M-FIRSTMATE' workspace, got '$CM1_WS_LABEL'" +pass "real herdr E2E: the primary-shaped home's crewmate landed in the '1M-FIRSTMATE' workspace" # --- 2. the PRIMARY spawns a secondmate: its tab lands in the SECONDMATE's own space --- # (fm-spawn.sh's herdr case arm shadows FM_HOME to the secondmate's home for @@ -158,7 +158,7 @@ SM_WSID=$(herdr pane get "$SM_PANE" --session "$SESSION" 2>/dev/null | jq -r '.r [ -n "$SM_WSID" ] || fail "could not read e2esm1's pane workspace_id" [ "$SM_WSID" != "$CM1_WSID" ] || fail "the secondmate's tab must NOT land in the primary's workspace, but it shares $CM1_WSID" SM_WS_LABEL=$(herdr workspace list --session "$SESSION" 2>&1 | jq -r --arg id "$SM_WSID" '.result.workspaces[]? | select(.workspace_id == $id) | .label') -[ "$SM_WS_LABEL" = "2ndmate-e2esm1" ] || fail "a --secondmate spawn should land in '2ndmate-', got '$SM_WS_LABEL'" +[ "$SM_WS_LABEL" = "2M-E2ESM1" ] || fail "a --secondmate spawn should land in '2M-', got '$SM_WS_LABEL'" pass "real herdr E2E: a --secondmate spawn by the PRIMARY lands in the SECONDMATE's own labeled workspace, distinct from the primary's" # --- 3. a crewmate spawned FROM the secondmate-shaped home lands in the SAME diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index 314bc8605c..cfccd922f0 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -230,8 +230,8 @@ test_workspace_label_primary_home_no_marker() { local home home="$TMP_ROOT/primary-home-no-marker"; mkdir -p "$home" out=$( FM_HOME="$home" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) - [ "$out" = "firstmate" ] || fail "a primary home (no .fm-secondmate-home marker) should resolve to label 'firstmate', got '$out'" - pass "fm_backend_herdr_workspace_label: a primary home (no marker) resolves to 'firstmate'" + [ "$out" = "1M-FIRSTMATE" ] || fail "a primary home (no .fm-secondmate-home marker) should resolve to label '1M-FIRSTMATE', got '$out'" + pass "fm_backend_herdr_workspace_label: a primary home (no marker) resolves to '1M-FIRSTMATE'" } test_workspace_label_secondmate_home_uses_marker_id() { @@ -239,8 +239,8 @@ test_workspace_label_secondmate_home_uses_marker_id() { home="$TMP_ROOT/secondmate-home"; mkdir -p "$home" printf 'sshhip-h7\n' > "$home/.fm-secondmate-home" out=$( FM_HOME="$home" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) - [ "$out" = "2ndmate-sshhip-h7" ] || fail "a secondmate home should resolve to '2ndmate-', got '$out'" - pass "fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2ndmate-'" + [ "$out" = "2M-SSHHIP-H7" ] || fail "a secondmate home should resolve to '2M-', got '$out'" + pass "fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2M-'" } test_workspace_label_secondmate_marker_trims_whitespace() { @@ -248,17 +248,17 @@ test_workspace_label_secondmate_marker_trims_whitespace() { home="$TMP_ROOT/secondmate-home-ws"; mkdir -p "$home" printf ' sshhip-h7 \n\n' > "$home/.fm-secondmate-home" out=$( FM_HOME="$home" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) - [ "$out" = "2ndmate-sshhip-h7" ] || fail "the marker id should be trimmed of surrounding whitespace, got '$out'" + [ "$out" = "2M-SSHHIP-H7" ] || fail "the marker id should be trimmed of surrounding whitespace, got '$out'" pass "fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id" } -test_workspace_label_empty_marker_falls_back_to_primary() { +test_workspace_label_empty_marker_falls_back_to_unknown_scope() { local home home="$TMP_ROOT/secondmate-home-empty"; mkdir -p "$home" : > "$home/.fm-secondmate-home" out=$( FM_HOME="$home" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) - [ "$out" = "firstmate" ] || fail "an empty/unreadable marker should fall back to 'firstmate', got '$out'" - pass "fm_backend_herdr_workspace_label: an empty marker file falls back to the primary label 'firstmate'" + [ "$out" = "2M-UNKNOWN" ] || fail "an empty/unreadable marker should fall back to '2M-UNKNOWN', got '$out'" + pass "fm_backend_herdr_workspace_label: an empty marker file still resolves as a secondmate (2M-UNKNOWN), never impersonating the primary's own 1M-FIRSTMATE label" } test_workspace_label_different_secondmates_get_different_labels() { @@ -267,12 +267,38 @@ test_workspace_label_different_secondmates_get_different_labels() { home2="$TMP_ROOT/secondmate-b"; mkdir -p "$home2"; printf 'bravo-b2\n' > "$home2/.fm-secondmate-home" out1=$( FM_HOME="$home1" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) out2=$( FM_HOME="$home2" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) - [ "$out1" = "2ndmate-alpha-a1" ] || fail "secondmate home1 label mismatch: $out1" - [ "$out2" = "2ndmate-bravo-b2" ] || fail "secondmate home2 label mismatch: $out2" + [ "$out1" = "2M-ALPHA-A1" ] || fail "secondmate home1 label mismatch: $out1" + [ "$out2" = "2M-BRAVO-B2" ] || fail "secondmate home2 label mismatch: $out2" [ "$out1" != "$out2" ] || fail "two different secondmate homes must not collide on the same label" pass "fm_backend_herdr_workspace_label: two different secondmate homes get two different, non-colliding labels" } +# --- mate_scope: id sanitization and fallback (herdr mate naming convention) -- + +test_mate_scope_uppercases_plain_id() { + out=$( bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_mate_scope "beadme"' "$ROOT" ) + [ "$out" = "BEADME" ] || fail "a plain lowercase id should just uppercase, got '$out'" + pass "fm_backend_herdr_mate_scope: uppercases a plain id" +} + +test_mate_scope_collapses_non_alnum_and_trims_edges() { + out=$( bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_mate_scope "-fm_dev--two.0-"' "$ROOT" ) + [ "$out" = "FM-DEV-TWO-0" ] || fail "non-alphanumeric runs should collapse to a single '-' and edges should trim, got '$out'" + pass "fm_backend_herdr_mate_scope: collapses non-alphanumeric characters and trims leading/trailing '-'" +} + +test_mate_scope_empty_id_falls_back_to_unknown() { + out=$( bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_mate_scope ""' "$ROOT" ) + [ "$out" = "UNKNOWN" ] || fail "an empty id should fall back to the literal 'UNKNOWN', got '$out'" + pass "fm_backend_herdr_mate_scope: an empty id falls back to 'UNKNOWN'" +} + +test_mate_scope_id_with_no_alnum_falls_back_to_unknown() { + out=$( bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_mate_scope "---..."' "$ROOT" ) + [ "$out" = "UNKNOWN" ] || fail "an id with no surviving alphanumeric character should fall back to 'UNKNOWN', got '$out'" + pass "fm_backend_herdr_mate_scope: an id that sanitizes to nothing falls back to 'UNKNOWN'" +} + # --- fm_backend_herdr_cli: session targeting (2026-07-02 incident fix) ------- test_cli_helper_sets_env_and_appends_trailing_session_flag() { @@ -456,13 +482,13 @@ test_workspace_ensure_prefers_the_launcher_over_the_first_label_match() { test_workspace_ensure_refuses_an_ambiguous_label_with_no_launcher() { local dir log resp fb out status dir="$TMP_ROOT/ensure-ambiguous"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" - printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"},{"workspace_id":"w7","label":"firstmate"}]}}\n' > "$resp/1.out" + printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w7","label":"1M-FIRSTMATE"}]}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_ensure fmtest /tmp' "$ROOT" 2>&1 ) status=$? expect_code 3 "$status" "two same-labeled home workspaces with no launcher identity must refuse" - assert_contains "$out" "labeled 'firstmate'" "the ambiguity refusal did not name the duplicated label" + assert_contains "$out" "labeled '1M-FIRSTMATE'" "the ambiguity refusal did not name the duplicated label" assert_contains "$out" "w1 w7" "the ambiguity refusal did not name the candidate workspaces" assert_not_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create' "an ambiguous placement must not mint a third same-labeled workspace" pass "fm_backend_herdr_workspace_ensure: refuses to guess between two same-labeled home workspaces" @@ -473,7 +499,7 @@ test_workspace_ensure_other_home_ignores_the_launcher_identity() { dir="$TMP_ROOT/ensure-other-home"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" # Only a workspace list: the launcher's own pane is never consulted, because a # --secondmate launch stands up a different home's workspace by design. - printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"}]}}\n' > "$resp/1.out" + printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"}]}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ HERDR_ENV=1 HERDR_PANE_ID=w7:p3 HERDR_SESSION=fmtest \ @@ -486,13 +512,13 @@ test_workspace_ensure_other_home_ignores_the_launcher_identity() { test_container_ensure_refuses_an_ambiguous_home_label() { local dir log resp fb out status dir="$TMP_ROOT/container-ambiguous"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" - printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"},{"workspace_id":"w7","label":"firstmate"}]}}\n' > "$resp/1.out" + printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w7","label":"1M-FIRSTMATE"}]}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /tmp' "$ROOT" 2>&1 ) status=$? [ "$status" -ne 0 ] || fail "container_ensure must fail when the home workspace is ambiguous" - assert_contains "$out" "labeled 'firstmate'" "container_ensure buried the specific ambiguity it refused" + assert_contains "$out" "labeled '1M-FIRSTMATE'" "container_ensure buried the specific ambiguity it refused" assert_not_contains "$out" "failed to ensure herdr workspace" "container_ensure added a generic message over the specific one" pass "fm_backend_herdr_container_ensure: surfaces the exact ambiguous-placement refusal instead of a generic failure" } @@ -509,19 +535,19 @@ test_container_ensure_starts_server_and_workspace() { # 3: `herdr server` backgrounded launch - no meaningful output # 4: server_ensure poll -> now running printf '{"server":{"running":true}}\n' > "$resp/4.out" - # 5: workspace list -> empty (no "firstmate" workspace yet) + # 5: workspace list -> empty (no "1M-FIRSTMATE" workspace yet) printf '{"result":{"workspaces":[]}}\n' > "$resp/5.out" # 6: workspace create -> w1, seeding default tab w1:t9 (real herdr returns # the seeded tab/pane ids in the SAME response - verified empirically). - printf '{"result":{"workspace":{"workspace_id":"w1","label":"firstmate"},"tab":{"tab_id":"w1:t9"},"root_pane":{"pane_id":"w1:p9"}}}\n' > "$resp/6.out" + printf '{"result":{"workspace":{"workspace_id":"w1","label":"1M-FIRSTMATE"},"tab":{"tab_id":"w1:t9"},"root_pane":{"pane_id":"w1:p9"}}}\n' > "$resp/6.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /tmp' "$ROOT" ) [ "$out" = $'fmtest:w1\tw1:t9' ] || fail "container_ensure should echo ':\\t', got '$out'" assert_contains "$(cat "$log")" "HERDR_SESSION=fmtest"$'\x1f''server' "container_ensure did not start the herdr server" - assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''firstmate' \ - "container_ensure did not create the firstmate workspace with the given cwd" - pass "fm_backend_herdr_container_ensure: version-gates, starts the server, ensures the firstmate workspace, echoes session:workspace_id + the seeded default tab id" + assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''1M-FIRSTMATE' \ + "container_ensure did not create the 1M-FIRSTMATE workspace with the given cwd" + pass "fm_backend_herdr_container_ensure: version-gates, starts the server, ensures the 1M-FIRSTMATE workspace, echoes session:workspace_id + the seeded default tab id" } test_container_ensure_reuses_existing_workspace() { @@ -529,13 +555,13 @@ test_container_ensure_reuses_existing_workspace() { dir="$TMP_ROOT/container-reuse"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" printf '{"client":{"version":"0.7.1","protocol":14}}\n' > "$resp/1.out" printf '{"server":{"running":true}}\n' > "$resp/2.out" - printf '{"result":{"workspaces":[{"workspace_id":"w9","label":"firstmate"}]}}\n' > "$resp/3.out" + printf '{"result":{"workspaces":[{"workspace_id":"w9","label":"1M-FIRSTMATE"}]}}\n' > "$resp/3.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /tmp' "$ROOT" ) - [ "$out" = $'fmtest:w9\t' ] || fail "container_ensure should reuse the existing firstmate workspace id with an EMPTY seeded-tab field (an ADOPTED workspace is never a prune candidate), got '$out'" + [ "$out" = $'fmtest:w9\t' ] || fail "container_ensure should reuse the existing 1M-FIRSTMATE workspace id with an EMPTY seeded-tab field (an ADOPTED workspace is never a prune candidate), got '$out'" assert_not_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create' "container_ensure should not create a workspace that already exists" - pass "fm_backend_herdr_container_ensure: reuses an existing firstmate workspace without recreating it, and reports no seeded default tab (adopted, not created)" + pass "fm_backend_herdr_container_ensure: reuses an existing 1M-FIRSTMATE workspace without recreating it, and reports no seeded default tab (adopted, not created)" } test_create_task_refuses_duplicate_label() { @@ -790,12 +816,12 @@ test_container_ensure_creates_with_no_focus_flag() { printf '{"client":{"version":"0.7.1","protocol":14}}\n' > "$resp/1.out" printf '{"server":{"running":true}}\n' > "$resp/2.out" printf '{"result":{"workspaces":[]}}\n' > "$resp/3.out" - printf '{"result":{"workspace":{"workspace_id":"w1","label":"firstmate"},"tab":{"tab_id":"w1:t1"},"root_pane":{"pane_id":"w1:p1"}}}\n' > "$resp/4.out" + printf '{"result":{"workspace":{"workspace_id":"w1","label":"1M-FIRSTMATE"},"tab":{"tab_id":"w1:t1"},"root_pane":{"pane_id":"w1:p1"}}}\n' > "$resp/4.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /tmp' "$ROOT" ) [ "$out" = $'fmtest:w1\tw1:t1' ] || fail "container_ensure should still echo ':\\t', got '$out'" - assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''firstmate'$'\x1f''--no-focus' \ + assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''1M-FIRSTMATE'$'\x1f''--no-focus' \ "container_ensure's workspace create did not pass --no-focus (focus-safety: never steal the captain's attention on spawn)" pass "fm_backend_herdr_container_ensure: workspace create passes --no-focus" } @@ -807,14 +833,14 @@ test_container_ensure_uses_secondmate_home_label() { printf '{"client":{"version":"0.7.1","protocol":14}}\n' > "$resp/1.out" printf '{"server":{"running":true}}\n' > "$resp/2.out" printf '{"result":{"workspaces":[]}}\n' > "$resp/3.out" - printf '{"result":{"workspace":{"workspace_id":"w9","label":"2ndmate-sshhip-h7"},"tab":{"tab_id":"w9:t1"},"root_pane":{"pane_id":"w9:p1"}}}\n' > "$resp/4.out" + printf '{"result":{"workspace":{"workspace_id":"w9","label":"2M-SSHHIP-H7"},"tab":{"tab_id":"w9:t1"},"root_pane":{"pane_id":"w9:p1"}}}\n' > "$resp/4.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HOME="$home" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /tmp' "$ROOT" ) [ "$out" = $'fmtest:w9\tw9:t1' ] || fail "container_ensure did not echo the expected session:workspace_id + seeded default tab id, got '$out'" - assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''2ndmate-sshhip-h7' \ + assert_contains "$(cat "$log")" $'\x1f''workspace'$'\x1f''create'$'\x1f''--cwd'$'\x1f''/tmp'$'\x1f''--label'$'\x1f''2M-SSHHIP-H7' \ "container_ensure did not create the workspace under this secondmate home's own label" - pass "fm_backend_herdr_container_ensure: creates the workspace under the SECONDMATE home's own label, not 'firstmate'" + pass "fm_backend_herdr_container_ensure: creates the workspace under the SECONDMATE home's own label, not '1M-FIRSTMATE'" } test_create_task_creates_with_no_focus_flag() { @@ -1186,6 +1212,36 @@ SH pass "herdr presentation ordering: secondmate children append under their owning parent block" } +test_projection_order_new_format_mate_parent_block() { + local dir log resp fb mover mover_log out status + dir="$TMP_ROOT/projection-order-new-format"; mkdir -p "$dir/responses" + log="$dir/log"; resp="$dir/responses"; mover="$dir/mover"; mover_log="$dir/mover.log" + : > "$log"; : > "$mover_log" + # 1M-FIRSTMATE, primary child, 2M-ALPHA, alpha-child (new format), 2M-BRAVO, human, NEW for alpha + printf '%s\n' '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w2","label":"└ primary · p:AbCdEfGhIjKlMnOpQrStUv"},{"workspace_id":"w3","label":"2M-ALPHA"},{"workspace_id":"w4","label":"└ old-a · p:AbCdEfGhIjKlMnOpQrStU1"},{"workspace_id":"w5","label":"2M-BRAVO"},{"workspace_id":"wH","label":"human-notes"},{"workspace_id":"w6","label":"└ new-a · p:ZyXwVuTsRqPoNmLkJiHgFe"}]}}' > "$resp/1.out" + printf '%s\n' '{"client":{"version":"0.7.4","protocol":16},"server":{"running":true}}' > "$resp/2.out" + # shellcheck disable=SC2016 + printf '%s\n' '{"schemas":{"request":{"oneOf":[{"properties":{"method":{"const":"workspace.move"}}}],"$defs":{"WorkspaceMoveParams":{"required":["workspace_id","insert_index"],"properties":{"insert_index":{"type":"integer"}}}}}}}' > "$resp/3.out" + printf '%s\n' '{"sessions":[{"name":"fmtest","running":true,"socket_path":"/tmp/fmtest.sock"}]}' > "$resp/4.out" + cat > "$mover" <<'SH' +#!/usr/bin/env bash +printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$FM_FAKE_MOVER_LOG" +printf '%s\n' '{"id":"fm-workspace-move","result":{"type":"workspace_list","workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w2","label":"└ primary · p:AbCdEfGhIjKlMnOpQrStUv"},{"workspace_id":"w3","label":"2M-ALPHA"},{"workspace_id":"w4","label":"└ old-a · p:AbCdEfGhIjKlMnOpQrStU1"},{"workspace_id":"w6","label":"└ new-a · p:ZyXwVuTsRqPoNmLkJiHgFe"},{"workspace_id":"w5","label":"2M-BRAVO"},{"workspace_id":"wH","label":"human-notes"}]}}' +SH + chmod +x "$mover" + fb=$(make_herdr_fakebin "$dir") + out=$(PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 \ + FM_BACKEND_HERDR_WORKSPACE_MOVER="$mover" FM_FAKE_MOVER_LOG="$mover_log" \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_projection_focus_snapshot() { printf "w5\tw5:t1"; }; fm_backend_herdr_projection_focus_restore() { return 0; }; fm_backend_herdr_projection_order_best_effort fmtest w6 2M-ALPHA' "$ROOT" 2>&1) + status=$? + [ "$status" -eq 0 ] || fail "new-format mate parent ordering must not fail the spawn: $out" + [ -z "$out" ] || fail "successful new-format mate ordering emitted a warning: $out" + [ "$(cat "$mover_log")" = "$(cd /tmp && pwd -P)/fmtest.sock"$'\t'"w6"$'\t'"4" ] \ + || fail "new-format mate child was not inserted after its parent block: $(cat "$mover_log")" + assert_not_contains "$(cat "$log")" $'workspace\x1frename' "new-format mate ordering renamed a workspace" + pass "herdr presentation ordering: new mate-naming-convention labels (1M-FIRSTMATE, 2M-ALPHA, 2M-BRAVO) are recognized as top-level parents and new children append under their owning block" +} + test_projection_order_foreign_legacy_child_is_read_only() { local dir log resp fb mover out status dir="$TMP_ROOT/projection-order-foreign-legacy"; mkdir -p "$dir/responses" @@ -1684,15 +1740,15 @@ test_workspace_find_matches_only_this_homes_own_label() { local dir log resp fb out home dir="$TMP_ROOT/find-scoped"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" home="$TMP_ROOT/find-scoped-home"; mkdir -p "$home"; printf 'bravo-b2\n' > "$home/.fm-secondmate-home" - # A workspace list carrying BOTH the primary's "firstmate" space and this - # secondmate's own "2ndmate-bravo-b2" space (as would be true once several + # A workspace list carrying BOTH the primary's "1M-FIRSTMATE" space and this + # secondmate's own "2M-BRAVO-B2" space (as would be true once several # homes share one herdr session) - find must pick the one matching THIS # home's own label, never the primary's or a sibling secondmate's. - printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"},{"workspace_id":"w2","label":"2ndmate-bravo-b2"},{"workspace_id":"w3","label":"2ndmate-alpha-a1"}]}}\n' > "$resp/1.out" + printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w2","label":"2M-BRAVO-B2"},{"workspace_id":"w3","label":"2M-ALPHA-A1"}]}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HOME="$home" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_find fmtest' "$ROOT" ) - [ "$out" = "w2" ] || fail "workspace_find should have matched this home's own label (2ndmate-bravo-b2 -> w2), got '$out'" + [ "$out" = "w2" ] || fail "workspace_find should have matched this home's own label (2M-BRAVO-B2 -> w2), got '$out'" pass "fm_backend_herdr_workspace_find: matches only THIS home's own label among several coexisting workspaces" } @@ -1703,7 +1759,7 @@ test_list_live_scoped_to_this_homes_workspace_only() { dir="$TMP_ROOT/list-live-scoped"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" home="$TMP_ROOT/list-live-scoped-home"; mkdir -p "$home"; printf 'bravo-b2\n' > "$home/.fm-secondmate-home" # 1: workspace_find's `workspace list` - two homes coexist, secondmate's is w2 - printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"},{"workspace_id":"w2","label":"2ndmate-bravo-b2"}]}}\n' > "$resp/1.out" + printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"1M-FIRSTMATE"},{"workspace_id":"w2","label":"2M-BRAVO-B2"}]}}\n' > "$resp/1.out" # 2: tab list --workspace w2 (this secondmate's own tabs only) printf '{"result":{"tabs":[{"tab_id":"w2:t1","label":"fm-secondmatetask"}]}}\n' > "$resp/2.out" # 3: pane_for_tab's `pane list --workspace w2` @@ -2634,9 +2690,9 @@ EOF bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_kill "$1"' "$ROOT" "fmtest:$pane" \ || fail "cycle $i: kill failed" done - # exactly one firstmate workspace survives three spawn/teardown cycles - wscount=$(jq -r '[.workspaces[]|select(.label=="firstmate")]|length' "$state") - [ "$wscount" = 1 ] || fail "expected exactly 1 firstmate workspace after 3 cycles, got $wscount: $(jq -c '.workspaces' "$state")" + # exactly one 1M-FIRSTMATE workspace survives three spawn/teardown cycles + wscount=$(jq -r '[.workspaces[]|select(.label=="1M-FIRSTMATE")]|length' "$state") + [ "$wscount" = 1 ] || fail "expected exactly 1 1M-FIRSTMATE workspace after 3 cycles, got $wscount: $(jq -c '.workspaces' "$state")" # and no orphaned workspaces of any label total=$(jq -r '.workspaces|length' "$state") [ "$total" = 1 ] || fail "expected no orphaned workspaces after 3 cycles, got $total total: $(jq -c '.workspaces' "$state")" @@ -2675,7 +2731,7 @@ test_adopted_workspace_never_prunes_default_tab() { # previous session created it), with a single tab labeled "1" - the same # shape herdr's own auto-seeded default tab has, but this run's own # container_ensure never ran a `workspace create` call to produce it. - jq -n '{next:2,workspaces:[{workspace_id:"w1",label:"firstmate"}],tabs:[{tab_id:"w1:t1",label:"1",workspace_id:"w1",pane_id:"w1:p1"}],agent_status:{}}' > "$state" + jq -n '{next:2,workspaces:[{workspace_id:"w1",label:"1M-FIRSTMATE"}],tabs:[{tab_id:"w1:t1",label:"1",workspace_id:"w1",pane_id:"w1:p1"}],agent_status:{}}' > "$state" raw=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_FAKE_HERDR_STATE="$state" HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /proj' "$ROOT" ) \ || fail "container_ensure failed against the stateful fake" @@ -2703,21 +2759,23 @@ EOF test_label_collision_startup_workspace_leaves_live_tab_alone() { # The exact live-fire incident shape (2026-07-02): a captain launches herdr - # directly inside a directory named "firstmate", so herdr auto-derives that - # workspace's DISPLAYED label from the cwd basename - "firstmate" - byte- - # identical to the primary firstmate home's own derived label, with no - # --label ever passed and no firstmate involvement at all. That workspace's - # single auto-created tab (label "1") holds the captain's own live agent. - # The very next crewmate spawn must adopt-and-leave-alone, never prune. + # directly inside a directory whose name coincidentally matches the primary + # firstmate home's own derived label ("firstmate" at the time of the + # incident, "1M-FIRSTMATE" under the current mate naming convention), so + # herdr auto-derives that workspace's DISPLAYED label from the cwd + # basename, byte-identical to firstmate's own label, with no --label ever + # passed and no firstmate involvement at all. That workspace's single + # auto-created tab (label "1") holds the captain's own live agent. The very + # next crewmate spawn must adopt-and-leave-alone, never prune. local dir log state fb raw container seeded ids pane dir="$TMP_ROOT/label-collision"; mkdir -p "$dir"; log="$dir/log"; state="$dir/state.json"; : > "$log" fb=$(make_herdr_statefake "$dir") - # Mimic a bare `herdr workspace create --cwd ` (no + # Mimic a bare `herdr workspace create --cwd ` (no # --label): the resulting workspace's label is the cwd basename, and its # one auto-created tab is still labeled "1" - indistinguishable, by label # alone, from firstmate's own freshly-seeded default tab. Its pane hosts a # live agent (agent_status=working), exactly like the captain's own pane. - jq -n '{next:2,workspaces:[{workspace_id:"w1",label:"firstmate"}],tabs:[{tab_id:"w1:t1",label:"1",workspace_id:"w1",pane_id:"w1:p1"}],agent_status:{"w1:p1":"working"}}' > "$state" + jq -n '{next:2,workspaces:[{workspace_id:"w1",label:"1M-FIRSTMATE"}],tabs:[{tab_id:"w1:t1",label:"1",workspace_id:"w1",pane_id:"w1:p1"}],agent_status:{"w1:p1":"working"}}' > "$state" raw=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_FAKE_HERDR_STATE="$state" HERDR_SESSION=fmtest \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_container_ensure /proj' "$ROOT" ) \ || fail "container_ensure failed against the stateful fake" @@ -3091,8 +3149,12 @@ test_version_check_refuses_missing_herdr test_workspace_label_primary_home_no_marker test_workspace_label_secondmate_home_uses_marker_id test_workspace_label_secondmate_marker_trims_whitespace -test_workspace_label_empty_marker_falls_back_to_primary +test_workspace_label_empty_marker_falls_back_to_unknown_scope test_workspace_label_different_secondmates_get_different_labels +test_mate_scope_uppercases_plain_id +test_mate_scope_collapses_non_alnum_and_trims_edges +test_mate_scope_empty_id_falls_back_to_unknown +test_mate_scope_id_with_no_alnum_falls_back_to_unknown test_cli_helper_sets_env_and_appends_trailing_session_flag test_launcher_identity_absent_without_a_herdr_pane test_launcher_identity_absent_when_herdr_env_alone_is_set @@ -3140,6 +3202,7 @@ test_projection_seeded_prune_refuses_active_tab test_projection_label_builder_uses_corner_and_strips_owner_prefixes test_projection_order_moves_only_exact_new_workspace_and_preserves_relative_order test_projection_order_secondmate_parent_block +test_projection_order_new_format_mate_parent_block test_projection_order_foreign_legacy_child_is_read_only test_projection_order_allows_intervening_parent_child_block test_projection_order_human_spaces_never_move_targets diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 5f2b0307ce..14d2756172 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -368,7 +368,8 @@ case "${1:-} ${2:-}" in printf '%s\n' '{"client":{"protocol":14,"version":"test"},"server":{"running":true}}' ;; "workspace list") - printf '{"result":{"workspaces":[{"workspace_id":"ws1","label":"2ndmate-%s"}]}}\n' "$mate_id" + printf '{"result":{"workspaces":[{"workspace_id":"ws1","label":"2M-%s"}]}}\n' \ + "$(printf '%s' "$mate_id" | tr '[:lower:]' '[:upper:]')" ;; "tab list") if [ -e "$spawned" ]; then From 62ff9762bdcc0e5e20c1afffb65db80b2e7777ab Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 19:27:41 -0700 Subject: [PATCH 2/5] no-mistakes(document): Fixed one stale doc-comment label reference; docs already in sync otherwise --- bin/backends/herdr.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 9ec9729a32..393153331e 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -885,7 +885,7 @@ fm_backend_herdr_workspace_find_all() { # # NOTE: the jq variable is $want, NOT $label - `label` is a jq reserved # keyword (label/break), so declaring a jq variable named "label" is a # compile error that `2>/dev/null` would silently swallow, making this find - # ALWAYS return empty and every spawn mint a fresh "firstmate" workspace + # ALWAYS return empty and every spawn mint a fresh "1M-FIRSTMATE" workspace # (the workspace leak). printf '%s' "$list" | jq -r --arg want "$label" \ '.result.workspaces[]? | select(.label == $want) | .workspace_id' 2>/dev/null From f72ed8bc607cf8b3f8b8b68a99d21a8bbd87918f Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 19:29:22 -0700 Subject: [PATCH 3/5] no-mistakes(document): Note frozen herdr E2E evidence predates mate naming convention --- docs/verification/runtime-backends.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/verification/runtime-backends.md b/docs/verification/runtime-backends.md index 40b8aac86a..c949eb36c1 100644 --- a/docs/verification/runtime-backends.md +++ b/docs/verification/runtime-backends.md @@ -228,6 +228,9 @@ ok - real herdr E2E: a --secondmate launch still stands up that secondmate's own ok - real herdr E2E: teardown closes only the worker's own pane and leaves the launcher, its workspace, and the same-labeled sibling intact ``` +This capture predates the mate naming convention (see `docs/herdr-backend.md`'s "Mate naming convention" section): it still shows the old `firstmate` workspace label rather than the current `1M-FIRSTMATE`. +The output above is preserved verbatim as the historical record; capture fresh evidence with the new label on the next real Herdr E2E run. + That suite's headline case runs `bin/fm-spawn.sh` inside a real Herdr pane, so the parent identity comes from Herdr's own injection rather than a composed environment. Cross-session and contradictory bindings are covered deterministically in `tests/fm-backend-herdr.test.sh`, which can script a second server's socket without provisioning one. From c3eda641ca6f3a526b56ea820ea5c0762daa38d0 Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 19:59:04 -0700 Subject: [PATCH 4/5] no-mistakes: apply CI fixes --- .../fm-backend-herdr-prune-safety-e2e.test.sh | 18 ++++++++++-------- ...ackend-herdr-workspace-per-home-e2e.test.sh | 10 ++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/fm-backend-herdr-prune-safety-e2e.test.sh b/tests/fm-backend-herdr-prune-safety-e2e.test.sh index f69160e0bd..792005e74e 100755 --- a/tests/fm-backend-herdr-prune-safety-e2e.test.sh +++ b/tests/fm-backend-herdr-prune-safety-e2e.test.sh @@ -7,7 +7,7 @@ # Reproduces the exact collision shape against a private, throwaway # HERDR_SESSION (never the captain's default): a startup-workspace-shaped # layout - one tab labeled "1" in a pre-existing workspace labeled -# "firstmate" - with a live long-running process in that pane, exactly as +# "1M-FIRSTMATE" - with a live long-running process in that pane, exactly as # the captain's own live crewmate session looked at incident time. Then # drives the real spawn-time container_ensure + # create_task path and asserts the live pane (and its live process) survive @@ -55,17 +55,19 @@ fm_backend_source herdr || fail "fm_backend_source herdr failed" fm_backend_herdr_version_check || fail "version_check failed against the real installed herdr" # --- 1. reproduce the label-collision startup-workspace shape --------------- -# Explicitly label the startup workspace "firstmate" to create the collision -# deterministically. Herdr's unlabeled workspace-label derivation is not a -# stable test contract, while the adopted-workspace state is the behavior -# this regression must exercise. The seeded tab remains labeled "1". +# Explicitly label the startup workspace "1M-FIRSTMATE" (the primary home's +# mate-naming-convention label; docs/herdr-backend.md "Mate naming +# convention") to create the collision deterministically. Herdr's unlabeled +# workspace-label derivation is not a stable test contract, while the +# adopted-workspace state is the behavior this regression must exercise. The +# seeded tab remains labeled "1". LIVE_CWD="$SCRATCH/firstmate" mkdir -p "$LIVE_CWD" fm_backend_herdr_server_ensure "$SESSION" || fail "could not start the isolated session's server" -CREATE_OUT=$(fm_backend_herdr_cli "$SESSION" workspace create --cwd "$LIVE_CWD" --label firstmate --no-focus) \ +CREATE_OUT=$(fm_backend_herdr_cli "$SESSION" workspace create --cwd "$LIVE_CWD" --label 1M-FIRSTMATE --no-focus) \ || fail "could not create the label-collision startup workspace" LIVE_WSID=$(printf '%s' "$CREATE_OUT" | jq -r '.result.workspace.workspace_id // empty') LIVE_TAB_ID=$(printf '%s' "$CREATE_OUT" | jq -r '.result.tab.tab_id // empty') @@ -75,8 +77,8 @@ if [ -z "$LIVE_WSID" ] || [ -z "$LIVE_TAB_ID" ] || [ -z "$LIVE_PANE_ID" ]; then fi LIVE_LABEL=$(herdr workspace list --session "$SESSION" 2>&1 | jq -r --arg id "$LIVE_WSID" '.result.workspaces[]? | select(.workspace_id == $id) | .label') -[ "$LIVE_LABEL" = firstmate ] || fail "the startup workspace label should be 'firstmate', got '$LIVE_LABEL' - repro setup is wrong" -pass "repro setup: a pre-existing workspace labeled 'firstmate' collides with the primary home's own label" +[ "$LIVE_LABEL" = 1M-FIRSTMATE ] || fail "the startup workspace label should be '1M-FIRSTMATE', got '$LIVE_LABEL' - repro setup is wrong" +pass "repro setup: a pre-existing workspace labeled '1M-FIRSTMATE' collides with the primary home's own label" # Simulate a live long-running agent in that pane: a heartbeat loop that # appends to a marker file, so liveness is independently verifiable (not just diff --git a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh index e76ff528d3..fc5da7a3c1 100755 --- a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +++ b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh @@ -197,10 +197,16 @@ assert_not_contains_local "$PRIMARY_LIVE" "fm-cm2" "the primary home's list_live pass "real herdr E2E: list_live from the primary's own context sees only the primary's own task" SM_LIVE=$(FM_HOME="$SM_HOME" fm_backend_herdr_list_live "$SESSION") -assert_contains_local "$SM_LIVE" "fm-e2esm1" "the secondmate home's list_live did not see its own task" +# e2esm1's own tab carries the mate naming convention's uppercase +# "-" label (2M-E2ESM1), not fm-e2esm1 - fm-spawn.sh labels a +# --secondmate spawn's own live-agent tab with its mate label instead of the +# ordinary fm- task label (docs/herdr-backend.md "Mate naming +# convention"), so list_live's fm- task filter deliberately does not +# surface it here. +assert_not_contains_local "$SM_LIVE" "fm-e2esm1" "the secondmate home's list_live must not see a fm--labeled tab for its own mate identity, which is labeled 2M-E2ESM1 instead" assert_contains_local "$SM_LIVE" "fm-cm2" "the secondmate home's list_live did not see the crewmate spawned from it" assert_not_contains_local "$SM_LIVE" "fm-cm1" "the secondmate home's list_live must not see the primary's task" -pass "real herdr E2E: list_live from the secondmate's own context sees only tasks in the secondmate's own workspace (both its own tab and its crewmate's)" +pass "real herdr E2E: list_live from the secondmate's own context sees only the fm- task tabs in its own workspace (its crewmate's, not its own mate tab)" # --- 5. teardown closes the RIGHT tab, and no other ------------------------ From 9fdd031cc5b3873d1c9176eedea65115fdef76ba Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sat, 1 Aug 2026 20:37:34 -0700 Subject: [PATCH 5/5] fix(herdr): address CodeRabbit findings on mate naming convention - correct comment/doc claims that the UNKNOWN scope fallback prevents collisions; it only guarantees non-empty, since every malformed marker shares the same UNKNOWN scope - preserve internal separators in a secondmate marker id by trimming only outer whitespace before normalization, instead of stripping all whitespace first - fix a stale legacy secondmate label in docs/herdr-backend.md (2ndmate-, not firstmate-) - reuse fm_backend_herdr_mate_scope in the fake herdr session-start test fixture instead of duplicating simplified normalization logic Addresses CodeRabbit review comments on PR #30. --- bin/backends/herdr.sh | 14 +++++++++++--- docs/herdr-backend.md | 4 ++-- tests/fm-backend-herdr.test.sh | 10 ++++++++++ tests/fm-session-start.test.sh | 9 ++++++--- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 393153331e..c1d91d155e 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -115,8 +115,11 @@ FM_BACKEND_HERDR_PRESENTATION_JOURNAL_SUFFIX=".herdr-presentation" # independently unit-testable: uppercases, then replaces every character # outside A-Z0-9 with "-", squeezes repeats, and trims leading/trailing "-". # An id that sanitizes to nothing (empty input, or no surviving alphanumeric) -# falls back to the literal "UNKNOWN" so a malformed or missing marker can -# never produce an empty or colliding workspace label. +# falls back to the literal "UNKNOWN" so a malformed or missing marker never +# produces an empty workspace label. That fallback only guarantees non-empty, +# not non-colliding: every malformed marker shares the same "UNKNOWN" scope, +# so two malformed homes can still collide on "2M-UNKNOWN" - fix the marker +# rather than relying on this fallback to stay unique. fm_backend_herdr_mate_scope() { local id=$1 scope scope=$(printf '%s' "$id" | tr '[:lower:]' '[:upper:]' | LC_ALL=C tr -c 'A-Z0-9' '-' | tr -s '-') @@ -141,7 +144,12 @@ fm_backend_herdr_mate_scope() { fm_backend_herdr_workspace_label() { local marker="$FM_HOME/$FM_BACKEND_HERDR_SECONDMATE_MARKER" id if [ -f "$marker" ]; then - id=$(tr -d '[:space:]' < "$marker" 2>/dev/null) + id=$(cat "$marker" 2>/dev/null) + # Trim only outer whitespace here; fm_backend_herdr_mate_scope is what + # normalizes any embedded separator (space, newline, ...) to "-", so + # stripping it here first would silently merge distinct ids together. + id="${id#"${id%%[![:space:]]*}"}" + id="${id%"${id##*[![:space:]]}"}" printf '2M-%s' "$(fm_backend_herdr_mate_scope "$id")" return 0 fi diff --git a/docs/herdr-backend.md b/docs/herdr-backend.md index bbbbd1d52e..4c3d93c9f7 100644 --- a/docs/herdr-backend.md +++ b/docs/herdr-backend.md @@ -58,7 +58,7 @@ That covers a missing or unusable socket identity, a closed or unreadable launch Firstmate running outside Herdr entirely has no launcher workspace to inherit, so its workers use this home's own labeled workspace, created on first use. That path needs the home label to identify exactly one workspace: two workspaces sharing it are an unresolvable placement and refuse rather than adopting either. Avoid naming a personal workspace `1M-FIRSTMATE` or `2M-` for that reason, and because the adapter cannot distinguish that label collision from its own container. -An older secondmate workspace using `firstmate-` is not migrated automatically; rename it manually before expecting new tasks or recovery to use it. +An older secondmate workspace using `2ndmate-` is not migrated automatically; rename it manually before expecting new tasks or recovery to use it. An installation upgrading from the pre-mate-naming-convention `firstmate`/`2ndmate-` labels is not migrated either: the first spawn into an already-running home mints a fresh `1M-FIRSTMATE`/`2M-` workspace rather than adopting the old one, leaving the old workspace behind to close or merge manually. Recovery and list-live still scan the first workspace matching the home label, because they address panes they already recorded rather than choosing where new work goes. @@ -71,7 +71,7 @@ Closing its last tab can remove the workspace, and the next spawn recreates it. A mate's workspace and its own live-agent tab are always labeled uppercase `-`, so the captain can tell a supervisor from a worker in Herdr's sidebar at a glance. `materank` is `1M` for the main firstmate, `2M` for a secondmate, `3M` for a third mate if one ever exists. `scope` is the mate's own registered id, uppercased: the primary defaults to the literal scope `FIRSTMATE` (giving `1M-FIRSTMATE`), and a secondmate's scope comes from its `.fm-secondmate-home` marker id (`bin/backends/herdr.sh`'s `fm_backend_herdr_mate_scope`), for example `2M-BEADME`. -That helper sanitizes the id (uppercase, non-alphanumeric characters collapsed to a single `-`, leading/trailing `-` trimmed) and falls back to the literal scope `UNKNOWN` when the id is empty or sanitizes to nothing, so a malformed or missing marker never produces an empty or colliding label. +That helper sanitizes the id (uppercase, non-alphanumeric characters collapsed to a single `-`, leading/trailing `-` trimmed) and falls back to the literal scope `UNKNOWN` when the id is empty or sanitizes to nothing, so a malformed or missing marker never produces an empty label; every malformed marker shares that same `UNKNOWN` scope, so two malformed homes can still collide on `2M-UNKNOWN`. A crewmate spawned by any mate inherits that mate's placement but is never uppercase: its tab keeps the ordinary lowercase `fm-` task label, so a mate and its subordinate crewmates are always visually distinct and never collide. ## Optional presentation spaces diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index cfccd922f0..6fb08fda46 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -252,6 +252,15 @@ test_workspace_label_secondmate_marker_trims_whitespace() { pass "fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id" } +test_workspace_label_secondmate_marker_preserves_embedded_separator() { + local home + home="$TMP_ROOT/secondmate-home-embedded-sep"; mkdir -p "$home" + printf ' alpha beta \n' > "$home/.fm-secondmate-home" + out=$( FM_HOME="$home" bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_workspace_label' "$ROOT" ) + [ "$out" = "2M-ALPHA-BETA" ] || fail "an embedded separator inside the marker id should survive outer-whitespace trimming and normalize to '-', got '$out'" + pass "fm_backend_herdr_workspace_label: preserves an embedded separator in the marker id through normalization" +} + test_workspace_label_empty_marker_falls_back_to_unknown_scope() { local home home="$TMP_ROOT/secondmate-home-empty"; mkdir -p "$home" @@ -3149,6 +3158,7 @@ test_version_check_refuses_missing_herdr test_workspace_label_primary_home_no_marker test_workspace_label_secondmate_home_uses_marker_id test_workspace_label_secondmate_marker_trims_whitespace +test_workspace_label_secondmate_marker_preserves_embedded_separator test_workspace_label_empty_marker_falls_back_to_unknown_scope test_workspace_label_different_secondmates_get_different_labels test_mate_scope_uppercases_plain_id diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 14d2756172..2596efe277 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -360,6 +360,7 @@ set -u log=${FM_FAKE_HERDR_LOG:?} state=${FM_FAKE_HERDR_STATE:?} mate_id=${FM_FAKE_SECOND_MATE_ID:?} +mate_scope=${FM_FAKE_SECOND_MATE_SCOPE:?} killed="${state}.killed" spawned="${state}.spawned" printf '%s\n' "$*" >> "$log" @@ -368,8 +369,7 @@ case "${1:-} ${2:-}" in printf '%s\n' '{"client":{"protocol":14,"version":"test"},"server":{"running":true}}' ;; "workspace list") - printf '{"result":{"workspaces":[{"workspace_id":"ws1","label":"2M-%s"}]}}\n' \ - "$(printf '%s' "$mate_id" | tr '[:lower:]' '[:upper:]')" + printf '{"result":{"workspaces":[{"workspace_id":"ws1","label":"2M-%s"}]}}\n' "$mate_scope" ;; "tab list") if [ -e "$spawned" ]; then @@ -545,9 +545,12 @@ EOF } run_session_start_herdr_secondmate() { - local root=$1 home=$2 fakebin=$3 mate=$4 log=$5 state=$6 + local root=$1 home=$2 fakebin=$3 mate=$4 log=$5 state=$6 scope + scope=$(bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_mate_scope "$1"' \ + "$ROOT" "$SESSION_START_HERDR_SECOND_MATE_ID") FM_BACKEND=herdr FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_STATE="$state" \ FM_FAKE_SECOND_MATE_ID="$SESSION_START_HERDR_SECOND_MATE_ID" \ + FM_FAKE_SECOND_MATE_SCOPE="$scope" \ run_session_start "$home" "$root" "$fakebin:$BASE_PATH" }