diff --git a/bin/fm-claude-trust.sh b/bin/fm-claude-trust.sh index 8fc3fb6b88a..833bcf0aa16 100755 --- a/bin/fm-claude-trust.sh +++ b/bin/fm-claude-trust.sh @@ -12,7 +12,8 @@ # fm-claude-trust.sh --secondmate-home # fm-claude-trust.sh --lab-home # the isolated task worktree this spawn launches into -# the primary checkout that worktree belongs to +# the primary checkout that worktree belongs to (or, for a pooled +# Treehouse slot, a clone of the same origin) # the seeded secondmate home this spawn launches into # the secondmate id that home must already be marked for # --lab-home the disposable lab home bin/fm-live-lab.sh launches a lab @@ -51,9 +52,10 @@ # (`es`/`F1e`) has no such fallback: it reads ONLY the canonical project-root # entry, and that root is never the worktree - Claude Code's own git-root # canonicalization (`Fr`/`Se`) walks a linked worktree's `.git` file through -# its `commondir` pointer back to the PRIMARY CHECKOUT, exactly the -# argument this script already receives for the worktree-mode scope test -# below. So the trust flag is registered on BOTH the worktree entry (for +# its `commondir` pointer back to the worktree's PRIMARY CHECKOUT, which may +# belong to another clone for a shared Treehouse pool slot. The scope test +# below resolves and verifies that checkout. So the trust flag is registered +# on BOTH the worktree entry (for # trust's ancestor-walk fallback and defense in depth) and the project entry # (the trust check's first, canonical-shaped, look); the two external-imports # flags land on those same two entries only when the project entry already @@ -89,8 +91,10 @@ # argument. Git is the ground truth, so the argument is never trusted on its # own word: a primary checkout (git dir == common dir), a worktree of an # unrelated repo, a subdirectory of a worktree, a plain directory, and a home -# directory are each refused. Refusal is a non-zero exit, never a warning and -# never a silent skip. When is itself a linked worktree (a +# directory are each refused. The one exception to the shared common dir is a +# Treehouse pool slot that is a worktree of another clone of 's origin +# (see the cross-clone block below). Refusal is a non-zero exit, never a +# warning and never a silent skip. When is itself a linked worktree (a # secondmate home spawned from, rather than as, the primary checkout), # refusing outright would wedge a relaunch that is otherwise perfectly valid: # its own common dir already IS the primary checkout's own git dir (git's @@ -228,6 +232,8 @@ refuse() { echo "error: refusing to pre-register Claude trust: $1" >&2; exit 1; # shellcheck source=bin/fm-gate-refuse-lib.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/fm-gate-refuse-lib.sh" +# shellcheck source=bin/fm-git-origin-lib.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/fm-git-origin-lib.sh" real_dir() { (cd -P -- "$1" 2>/dev/null && pwd -P); } @@ -298,34 +304,57 @@ if [ "$MODE" = worktree ]; then PROJ_COMMON=$(common_dir_of "$PROJ_REAL") || true [ -n "$PROJ_COMMON" ] || refuse "project '$PROJ_REAL' is not inside a git repository" - [ "$WT_COMMON" = "$PROJ_COMMON" ] || refuse "'$TARGET_REAL' is not a worktree of project '$PROJ_REAL'" + # Treehouse shares one pool among every clone of an origin, so a secondmate + # home's spawn can be handed a slot that is a worktree of another home's clone + # (the root home's, say). That slot is accepted only on positive evidence: it + # must sit in a Treehouse pool (the pool's own state file beside its slot + # directory) and its repository must share a non-empty origin with . + # An unrelated repository, a pool-less linked worktree, and a primary + # checkout are still refused. The project entry written below is then the + # slot's own canonical checkout, because that is where Claude Code's git-root + # canonicalization sends the worktree and so where the imports check reads. + CROSS_CLONE=0 + if [ "$WT_COMMON" != "$PROJ_COMMON" ]; then + WT_POOL_STATE="$(dirname -- "$(dirname -- "$TARGET_REAL")")/treehouse-state.json" + if [ -f "$WT_POOL_STATE" ] && [ ! -L "$WT_POOL_STATE" ] && fm_git_same_origin "$TARGET_REAL" "$PROJ_REAL"; then + CROSS_CLONE=1 + else + refuse "'$TARGET_REAL' is not a worktree of project '$PROJ_REAL'" + fi + fi # The external-imports flags must land on the primary checkout - its own git # dir equals the common dir - because that is exactly the path Claude Code's # own git-root canonicalization collapses every linked worktree to. When # is itself a linked worktree (a secondmate home spawned from, # rather than as, the primary checkout), refusing outright would wedge a - # relaunch that is otherwise perfectly valid: PROJ_COMMON already IS that - # primary checkout's own git dir (git's git-common-dir answer never changes - # by which worktree asks), so the checkout is derived structurally from it - + # relaunch that is otherwise perfectly valid: the selected CANON_BASE already + # IS that primary checkout's own git dir (WT_COMMON for a cross-clone slot, + # PROJ_COMMON otherwise), so the checkout is derived structurally from it - # its parent directory in the standard non-bare, non-GIT_DIR-overridden # layout this script already requires elsewhere - and verified, never - # assumed: the candidate's own resolved git dir must equal PROJ_COMMON, the + # assumed: the candidate's own resolved git dir must equal CANON_BASE, the # same primary-checkout definition used above, or this refuses rather than # guess. - PROJ_GIT_DIR=$(git -C "$PROJ_REAL" rev-parse --absolute-git-dir 2>/dev/null) || true - [ -n "$PROJ_GIT_DIR" ] || refuse "project '$PROJ_REAL' has no resolvable git directory" - PROJ_GIT_DIR=$(real_dir "$PROJ_GIT_DIR") || true - [ -n "$PROJ_GIT_DIR" ] || refuse "project '$PROJ_REAL' has an unresolvable git directory" - if [ "$PROJ_GIT_DIR" = "$PROJ_COMMON" ]; then + if [ "$CROSS_CLONE" = 1 ]; then + CANON_BASE=$WT_COMMON + PROJ_GIT_DIR= + else + CANON_BASE=$PROJ_COMMON + PROJ_GIT_DIR=$(git -C "$PROJ_REAL" rev-parse --absolute-git-dir 2>/dev/null) || true + [ -n "$PROJ_GIT_DIR" ] || refuse "project '$PROJ_REAL' has no resolvable git directory" + PROJ_GIT_DIR=$(real_dir "$PROJ_GIT_DIR") || true + [ -n "$PROJ_GIT_DIR" ] || refuse "project '$PROJ_REAL' has an unresolvable git directory" + fi + if [ "$PROJ_GIT_DIR" = "$CANON_BASE" ]; then PROJ_CANON=$PROJ_REAL else - PROJ_CANON=$(real_dir "$(dirname -- "$PROJ_COMMON")") || true + PROJ_CANON=$(real_dir "$(dirname -- "$CANON_BASE")") || true [ -n "$PROJ_CANON" ] \ || refuse "project '$PROJ_REAL' is a linked worktree whose primary checkout could not be resolved" CANON_GIT_DIR=$(git -C "$PROJ_CANON" rev-parse --absolute-git-dir 2>/dev/null) || true CANON_GIT_DIR=$(real_dir "${CANON_GIT_DIR:-}") || true - [ -n "$CANON_GIT_DIR" ] && [ "$CANON_GIT_DIR" = "$PROJ_COMMON" ] \ + [ -n "$CANON_GIT_DIR" ] && [ "$CANON_GIT_DIR" = "$CANON_BASE" ] \ || refuse "project '$PROJ_REAL' is a linked worktree whose primary checkout could not be resolved" fi elif [ "$MODE" = lab-home ]; then diff --git a/bin/fm-git-origin-lib.sh b/bin/fm-git-origin-lib.sh new file mode 100644 index 00000000000..a643672fdb2 --- /dev/null +++ b/bin/fm-git-origin-lib.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# fm-git-origin-lib.sh - the shared notion of "the same repository" across +# separate clones, with no source-time side effects. +# +# Treehouse keys one worktree pool by repository name plus a hash of the +# origin URL, so every home on this machine whose project clone has the same +# origin draws from one pool: a slot handed to a secondmate home may be a +# worktree of the root home's clone (see fm_treehouse_project_lock_path in +# fm-wake-lib.sh, which serializes that shared pool). Callers that must decide +# whether a pooled worktree belongs to a project compare this identity rather +# than git common dirs alone. + +# Print the resolved origin identity of the repository containing , or +# fail when it has no origin. A local origin path is resolved to its physical +# path so two spellings of one bare repository agree. +fm_git_origin_identity() { # + local dir=$1 origin + [ -d "$dir" ] || return 1 + origin=$(git -C "$dir" remote get-url origin 2>/dev/null || true) + [ -n "$origin" ] || return 1 + case "$origin" in + /*) [ ! -d "$origin" ] || origin=$(CDPATH='' cd -- "$origin" 2>/dev/null && pwd -P) || return 1 ;; + *://*|*:* ) ;; + *) [ ! -d "$dir/$origin" ] || origin=$(CDPATH='' cd -- "$dir/$origin" 2>/dev/null && pwd -P) || return 1 ;; + esac + printf '%s\n' "$origin" +} + +# Succeed when both directories are in repositories that share a non-empty +# origin identity. +fm_git_same_origin() { # + local a b + a=$(fm_git_origin_identity "$1") || return 1 + b=$(fm_git_origin_identity "$2") || return 1 + [ "$a" = "$b" ] +} diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index 24ed4644c76..234128e31fc 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -343,6 +343,7 @@ for _teardown_source in \ fm-nm-run-lib.sh \ fm-wake-lib.sh \ fm-path-lib.sh \ + fm-git-origin-lib.sh \ fm-lease-lib.sh do teardown_require_source "$SCRIPT_DIR/$_teardown_source" diff --git a/bin/fm-wake-lib.sh b/bin/fm-wake-lib.sh index 60a9d289090..5b63b1cd6fd 100755 --- a/bin/fm-wake-lib.sh +++ b/bin/fm-wake-lib.sh @@ -12,6 +12,8 @@ FM_WAKE_QUEUE_LOCK="${FM_WAKE_QUEUE_LOCK:-$STATE/.wake-queue.lock}" FM_LOCK_STALE_AFTER="${FM_LOCK_STALE_AFTER:-2}" # shellcheck source=bin/fm-path-lib.sh . "$FM_WAKE_LIB_DIR/fm-path-lib.sh" +# shellcheck source=bin/fm-git-origin-lib.sh +. "$FM_WAKE_LIB_DIR/fm-git-origin-lib.sh" # Resolved once at source time: fm_pid_identity and fm_path_mtime run inside 0.2s # confirm and 0.5s attach polls, and forking uname per call is a measurable cost on # the platform (Git Bash/MSYS) that already pays the highest fork price. @@ -1403,18 +1405,10 @@ fm_firstmate_root_home() { # separate clones of one origin share a single lock; an origin-less local-only # project falls back to its own worktree top instead of failing to resolve. fm_treehouse_project_lock_path() { # - local project=$1 root origin identity hash top + local project=$1 root identity hash top [ -d "$project" ] || return 1 root=$(fm_firstmate_root_home "$FM_HOME") || return 1 - origin=$(git -C "$project" remote get-url origin 2>/dev/null || true) - if [ -n "$origin" ]; then - case "$origin" in - /*) [ ! -d "$origin" ] || origin=$(CDPATH='' cd -- "$origin" 2>/dev/null && pwd -P) || return 1 ;; - *://*|*:* ) ;; - *) [ ! -d "$project/$origin" ] || origin=$(CDPATH='' cd -- "$project/$origin" 2>/dev/null && pwd -P) || return 1 ;; - esac - identity=$origin - else + if ! identity=$(fm_git_origin_identity "$project"); then top=$(git -C "$project" rev-parse --show-toplevel 2>/dev/null) || return 1 top=$(CDPATH='' cd -- "$top" 2>/dev/null && pwd -P) || return 1 identity=$top @@ -1426,7 +1420,8 @@ fm_treehouse_project_lock_path() { # # A Treehouse slot has the managed pool's fixed // layout. # Require both its pool state and the same Git common directory as the recorded -# project; an ordinary linked worktree is not evidence that Treehouse owns it. +# project, or a clone of the same origin because the pool is shared across homes; +# an ordinary linked worktree is not evidence that Treehouse owns it. fm_treehouse_pool_slot() { # local project=$1 worktree=$2 slot pool state project_common slot_common [ -d "$project" ] && [ -d "$worktree" ] || return 1 @@ -1438,7 +1433,10 @@ fm_treehouse_pool_slot() { # slot_common=$(git -C "$slot" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 1 project_common=$(CDPATH='' cd -- "$project_common" 2>/dev/null && pwd -P) || return 1 slot_common=$(CDPATH='' cd -- "$slot_common" 2>/dev/null && pwd -P) || return 1 - [ "$project_common" = "$slot_common" ] + [ "$project_common" = "$slot_common" ] && return 0 + # The pool is shared by every clone of one origin, so a slot may be a + # worktree of another home's clone of this project. + fm_git_same_origin "$project" "$slot" } # Slot-owner claim: which task a Treehouse pool slot currently belongs to. diff --git a/docs/orca-backend.md b/docs/orca-backend.md index 000782a3536..8a01a0f46d9 100644 --- a/docs/orca-backend.md +++ b/docs/orca-backend.md @@ -75,7 +75,7 @@ Reinstall the CLI and rerun; [`verification/runtime-backends.md`](verification/r - Escape is unsupported. - Orca exposes no stable CLI version or protocol marker, so readiness is the compatibility gate rather than a version floor. - Only the verified terminal-handle and worktree result fields are accepted; speculative response shapes are rejected. -- Orca's worktree shape is unverified against the spawn-time Claude workspace-trust check in `bin/fm-claude-trust.sh`, which refuses any path that is not a linked git worktree sharing the project's git common dir, so a claude spawn on Orca fails loudly at that check rather than launching if Orca clones instead of linking. +- Orca's worktree shape is unverified against the spawn-time Claude workspace-trust check; [`fm-claude-trust.sh`](../bin/fm-claude-trust.sh)'s header owns the scope contract, and a claude spawn on Orca fails loudly at that check rather than launching if Orca clones instead of linking. ## Regression entry points diff --git a/docs/scripts.md b/docs/scripts.md index 2c6b161a968..03cd0ae7291 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -120,6 +120,7 @@ The shared no-mistakes gate lifecycle boundary is summarized in [architecture.md | `fm-wake-grant.sh` | Serialize Pi supervision-branch wake-row claim activation, publication, release, and deactivation | | `fm-wake-lib.sh` | Shared durable wake queue, recovery generations, portable locks, and watcher identity/health helpers | | `fm-path-lib.sh` | Fork-free `dirname`/`basename` equivalents with no source-time side effects | +| `fm-git-origin-lib.sh` | Shared origin-identity helpers so separate clones of one origin (a shared Treehouse pool) count as the same repository | | `fm-classify-lib.sh` | Shared wake classification, durable keyed-decision folds and scans, unread status selection, home-owned status-append ranges, and bounded latest-event snapshots | | `fm-send.sh` | Steer a task via a durable inbox record plus doorbell, or send a supported key or typed harness invocation through the recorded backend | | `fm-branch-prompt.sh` | Emit the shared supervision branch's byte-stable system prompt ([pi-supervision-branch.md](pi-supervision-branch.md), [supervision-host.md](supervision-host.md)) | diff --git a/tests/fm-afk-return.test.sh b/tests/fm-afk-return.test.sh index fd589be2965..2a553cffa65 100755 --- a/tests/fm-afk-return.test.sh +++ b/tests/fm-afk-return.test.sh @@ -26,6 +26,7 @@ install_runner() { # cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/" cp "$ROOT/bin/fm-lock-lib.sh" "$dir/bin/" cp "$ROOT/bin/fm-path-lib.sh" "$dir/bin/" + cp "$ROOT/bin/fm-git-origin-lib.sh" "$dir/bin/" cp "$ROOT/bin/fm-classify-lib.sh" "$dir/bin/" # fm-timeout-lib.sh: the shared hard bound fm-classify-lib.sh sources for the # wedge detector's bounded worktree write probe. diff --git a/tests/fm-claude-stop-autoarm.test.sh b/tests/fm-claude-stop-autoarm.test.sh index 7b47c25e854..bf080fcb781 100755 --- a/tests/fm-claude-stop-autoarm.test.sh +++ b/tests/fm-claude-stop-autoarm.test.sh @@ -31,6 +31,7 @@ install_autoarm_scripts() { cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh" cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh" cp "$ROOT/bin/fm-path-lib.sh" "$dir/bin/fm-path-lib.sh" + cp "$ROOT/bin/fm-git-origin-lib.sh" "$dir/bin/fm-git-origin-lib.sh" cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh" cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh" cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh" diff --git a/tests/fm-claude-trust.test.sh b/tests/fm-claude-trust.test.sh index 70a807c04fa..35ce99b6983 100755 --- a/tests/fm-claude-trust.test.sh +++ b/tests/fm-claude-trust.test.sh @@ -447,6 +447,81 @@ test_foreign_project_worktree_is_refused() { pass "fm-claude-trust.sh: refuses a worktree belonging to another project" } +# A Treehouse pool is shared by every clone of one origin, so a secondmate +# home's spawn can be handed a slot that is a worktree of another home's clone. +# pooled_slot_of_other_clone : a pool laid out +# as // with its state file, holding a worktree of . +pooled_slot_of_other_clone() { + local case_dir=$1 main=$2 slot=$3 pool="$1/pool" + mkdir -p "$pool/$slot" + printf '{"worktrees":[]}\n' > "$pool/treehouse-state.json" + git -C "$main" worktree add --quiet --detach "$pool/$slot/project" + printf '%s\n' "$pool/$slot/project" +} + +test_pooled_worktree_of_another_clone_of_the_same_origin_is_trusted() { + local rec out main_wt second + rec=$(make_case same-origin-pool) + read_case "$rec" + main_wt=$(pooled_slot_of_other_clone "$CASE_DIR" "$PROJ" 1) + second="$CASE_DIR/second-home-project" + git clone --quiet "file://$PROJ.origin.git" "$second" + out=$(run_trust "$CONFIG" "$main_wt" "$second") + expect_code 0 $? "a pooled worktree of another clone of the same origin must be trusted: $out" + assert_trusted "$CONFIG/.claude.json" "$main_wt" "the pooled worktree was not trusted" + assert_trusted "$CONFIG/.claude.json" "$PROJ" "the slot's own canonical checkout was not trusted" + assert_not_trusted "$CONFIG/.claude.json" "$second" "the second home's clone was trusted though the worktree does not belong to it" + pass "fm-claude-trust.sh: trusts a pooled worktree of another clone of the same origin" +} + +test_cross_clone_worktree_outside_a_pool_is_refused() { + local rec out second + rec=$(make_case same-origin-no-pool) + read_case "$rec" + second="$CASE_DIR/second-home-project" + git clone --quiet "file://$PROJ.origin.git" "$second" + out=$(run_trust "$CONFIG" "$WT" "$second") + expect_code 1 $? "a non-pooled worktree of another clone must be refused: $out" + assert_contains "$out" "is not a worktree of project" "the refusal did not name the project mismatch" + assert_not_trusted "$CONFIG/.claude.json" "$WT" "a non-pooled foreign worktree was trusted" + pass "fm-claude-trust.sh: refuses another clone's worktree that is not in a Treehouse pool" +} + +test_pooled_worktree_of_an_unrelated_origin_is_refused() { + local rec out pooled other + rec=$(make_case pool-unrelated-origin) + read_case "$rec" + other="$CASE_DIR/other-project" + fm_git_init_commit "$other" + fm_git_add_origin "$other" "$other.origin.git" + pooled=$(pooled_slot_of_other_clone "$CASE_DIR" "$other" 1) + out=$(run_trust "$CONFIG" "$pooled" "$PROJ") + expect_code 1 $? "a pooled worktree of an unrelated origin must be refused: $out" + assert_contains "$out" "is not a worktree of project" "the refusal did not name the project mismatch" + assert_not_trusted "$CONFIG/.claude.json" "$pooled" "a pooled unrelated worktree was trusted" + pass "fm-claude-trust.sh: refuses a pooled worktree of an unrelated origin" +} + +test_pool_slot_recognizes_another_clone_of_the_same_origin() { + local rec main_wt second other pooled_other + rec=$(make_case pool-slot-origin) + read_case "$rec" + main_wt=$(pooled_slot_of_other_clone "$CASE_DIR" "$PROJ" 1) + second="$CASE_DIR/second-home-project" + git clone --quiet "file://$PROJ.origin.git" "$second" + other="$CASE_DIR/other-project" + fm_git_init_commit "$other" + fm_git_add_origin "$other" "$other.origin.git" + pooled_other=$(pooled_slot_of_other_clone "$CASE_DIR" "$other" 2) + # shellcheck disable=SC2016 + FM_HOME="$CASE_DIR" FM_ROOT="$ROOT" bash -c '. "$FM_ROOT/bin/fm-wake-lib.sh"; fm_treehouse_pool_slot "$1" "$2"' _ "$second" "$main_wt" \ + || fail "a pool slot of another clone of the same origin was not recognized as the project's slot" + # shellcheck disable=SC2016 + FM_HOME="$CASE_DIR" FM_ROOT="$ROOT" bash -c '. "$FM_ROOT/bin/fm-wake-lib.sh"; fm_treehouse_pool_slot "$1" "$2"' _ "$second" "$pooled_other" \ + && fail "a pool slot of an unrelated origin was recognized as the project's slot" + pass "fm_treehouse_pool_slot: matches slots by origin across clones and still rejects unrelated ones" +} + test_worktree_subdirectory_is_refused() { local rec out sub rec=$(make_case subdir) @@ -856,6 +931,10 @@ test_relative_config_dir_is_refused test_non_git_directory_is_refused test_missing_directory_is_refused test_foreign_project_worktree_is_refused +test_pooled_worktree_of_another_clone_of_the_same_origin_is_trusted +test_cross_clone_worktree_outside_a_pool_is_refused +test_pooled_worktree_of_an_unrelated_origin_is_refused +test_pool_slot_recognizes_another_clone_of_the_same_origin test_worktree_subdirectory_is_refused test_project_argument_that_is_itself_a_worktree_resolves_to_the_primary_checkout test_unrelated_store_content_is_preserved diff --git a/tests/fm-cursor-primary.test.sh b/tests/fm-cursor-primary.test.sh index df024fe3ae2..ee67f09eb2f 100755 --- a/tests/fm-cursor-primary.test.sh +++ b/tests/fm-cursor-primary.test.sh @@ -72,7 +72,7 @@ install_scripts() { for f in fm-turnend-guard-cursor.sh fm-turnend-guard.sh fm-sessionstart-cursor.sh \ fm-sessionstart-run.sh fm-sessionstart-nudge.sh fm-arm-pretool-check.sh \ fm-cd-pretool-check.sh fm-claude-stop-autoarm.sh fm-hook-host-lib.sh \ - fm-primary-scope-lib.sh fm-supervision-lib.sh fm-wake-lib.sh fm-path-lib.sh \ + fm-primary-scope-lib.sh fm-supervision-lib.sh fm-wake-lib.sh fm-path-lib.sh fm-git-origin-lib.sh \ fm-session-lock-lib.sh fm-cursor-lib.sh fm-operational-input.sh \ fm-supervision-instructions.sh fm-harness.sh fm-lock.sh \ fm-gate-refuse-lib.sh fm-afk-contract.sh fm-classify-lib.sh fm-timeout-lib.sh \ diff --git a/tests/fm-gotmp.test.sh b/tests/fm-gotmp.test.sh index bd889a4306b..093fbe0356b 100755 --- a/tests/fm-gotmp.test.sh +++ b/tests/fm-gotmp.test.sh @@ -77,6 +77,7 @@ SH ln -s "$ROOT/bin/fm-timeout-lib.sh" "$fake/bin/fm-timeout-lib.sh" ln -s "$ROOT/bin/fm-wake-lib.sh" "$fake/bin/fm-wake-lib.sh" ln -s "$ROOT/bin/fm-path-lib.sh" "$fake/bin/fm-path-lib.sh" + ln -s "$ROOT/bin/fm-git-origin-lib.sh" "$fake/bin/fm-git-origin-lib.sh" # fm-gate-refuse-lib.sh: teardown sources it before any fleet mutation. ln -s "$ROOT/bin/fm-gate-refuse-lib.sh" "$fake/bin/fm-gate-refuse-lib.sh" # fm-pr-lib.sh: teardown uses its canonical task-ID validator for poll cleanup. @@ -182,6 +183,7 @@ SH ln -s "$ROOT/bin/fm-timeout-lib.sh" "$fake/bin/fm-timeout-lib.sh" ln -s "$ROOT/bin/fm-wake-lib.sh" "$fake/bin/fm-wake-lib.sh" ln -s "$ROOT/bin/fm-path-lib.sh" "$fake/bin/fm-path-lib.sh" + ln -s "$ROOT/bin/fm-git-origin-lib.sh" "$fake/bin/fm-git-origin-lib.sh" # fm-gate-refuse-lib.sh: teardown sources it before any fleet mutation. ln -s "$ROOT/bin/fm-gate-refuse-lib.sh" "$fake/bin/fm-gate-refuse-lib.sh" # fm-pr-lib.sh: teardown uses its canonical task-ID validator for poll cleanup. diff --git a/tests/fm-mail-check.test.sh b/tests/fm-mail-check.test.sh index 72374a98bbc..ee73f63d849 100644 --- a/tests/fm-mail-check.test.sh +++ b/tests/fm-mail-check.test.sh @@ -63,6 +63,7 @@ enter_mailbox() { mkdir -p "$home/bin" "$FAKEBIN" [ -e "$home/bin/fm-wake-lib.sh" ] || ln -s "$ROOT/bin/fm-wake-lib.sh" "$home/bin/fm-wake-lib.sh" [ -e "$home/bin/fm-path-lib.sh" ] || ln -s "$ROOT/bin/fm-path-lib.sh" "$home/bin/fm-path-lib.sh" + [ -e "$home/bin/fm-git-origin-lib.sh" ] || ln -s "$ROOT/bin/fm-git-origin-lib.sh" "$home/bin/fm-git-origin-lib.sh" printf '%s\n' "$generator" > "$FAKEBIN/python3" chmod +x "$FAKEBIN/python3" } diff --git a/tests/fm-pi-branch-extension.test.sh b/tests/fm-pi-branch-extension.test.sh index 56d5a25e408..6af07ad923a 100644 --- a/tests/fm-pi-branch-extension.test.sh +++ b/tests/fm-pi-branch-extension.test.sh @@ -1638,7 +1638,8 @@ test_branch_default_on_heartbeat_afk_and_fallback() { install_pi_branch_extension_fixture "$repo" cp "$ROOT/bin/fm-branch-outcome.sh" "$ROOT/bin/fm-classify-lib.sh" \ "$ROOT/bin/fm-lease.sh" "$ROOT/bin/fm-lease-lib.sh" "$ROOT/bin/fm-timeout-lib.sh" \ - "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$ROOT/bin/fm-wake-grant.sh" "$broken/bin/" + "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$ROOT/bin/fm-git-origin-lib.sh" \ + "$ROOT/bin/fm-wake-grant.sh" "$broken/bin/" cat > "$broken/bin/fm-branch-prompt.sh" <<'SH' #!/usr/bin/env bash echo "synthetic generator failure" >&2 diff --git a/tests/fm-remote-backlog-handoff.test.sh b/tests/fm-remote-backlog-handoff.test.sh index c730e380c4f..2297f5ad820 100755 --- a/tests/fm-remote-backlog-handoff.test.sh +++ b/tests/fm-remote-backlog-handoff.test.sh @@ -53,7 +53,8 @@ printf 'fixture\n' > "$REMOTE_ROOT/AGENTS.md" cp "$ROOT/bin/fm-remote-entrypoint.sh" "$ROOT/bin/fm-remote-job-lib.sh" \ "$ROOT/bin/fm-remote-job-worker.sh" "$ROOT/bin/fm-remote-file.sh" \ "$ROOT/bin/fm-backlog-receive.sh" "$ROOT/bin/fm-tasks-axi-lib.sh" \ - "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$REMOTE_ROOT/bin/" + "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$ROOT/bin/fm-git-origin-lib.sh" \ + "$REMOTE_ROOT/bin/" ln -s "$(command -v tasks-axi)" "$REMOTE_ROOT/bin/tasks-axi" ln -s "$(command -v node)" "$REMOTE_ROOT/bin/node" chmod +x "$REMOTE_ROOT/bin"/*.sh diff --git a/tests/fm-remote-transport-lanes.test.sh b/tests/fm-remote-transport-lanes.test.sh index 02dc44c8676..055650b50e4 100755 --- a/tests/fm-remote-transport-lanes.test.sh +++ b/tests/fm-remote-transport-lanes.test.sh @@ -51,7 +51,8 @@ cp "$ROOT/bin/fm-remote-job-lib.sh" "$ROOT/bin/fm-remote-job-worker.sh" \ "$ROOT/bin/fm-remote-entrypoint.sh" "$ROOT/bin/fm-remote-delta-read.sh" \ "$ROOT/bin/fm-remote-secondmate-control.sh" "$ROOT/bin/fm-backend.sh" \ "$ROOT/bin/fm-pending-reply-lib.sh" "$ROOT/bin/fm-task-inbox-lib.sh" \ - "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$ROOT/bin/fm-marker-lib.sh" \ + "$ROOT/bin/fm-wake-lib.sh" "$ROOT/bin/fm-path-lib.sh" "$ROOT/bin/fm-git-origin-lib.sh" \ + "$ROOT/bin/fm-marker-lib.sh" \ "$ROOT/bin/fm-operational-input.sh" "$ROOT/bin/fm-tmux-lib.sh" \ "$ROOT/bin/fm-composer-lib.sh" "$ROOT/bin/fm-cursor-lib.sh" \ "$ROOT/bin/fm-classify-lib.sh" "$ROOT/bin/fm-timeout-lib.sh" \ diff --git a/tests/fm-session-lock-ancestry.test.sh b/tests/fm-session-lock-ancestry.test.sh index 6076057bb17..82a8b4940bd 100755 --- a/tests/fm-session-lock-ancestry.test.sh +++ b/tests/fm-session-lock-ancestry.test.sh @@ -437,6 +437,7 @@ install_autoarm_scripts() { cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh" cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh" cp "$ROOT/bin/fm-path-lib.sh" "$dir/bin/fm-path-lib.sh" + cp "$ROOT/bin/fm-git-origin-lib.sh" "$dir/bin/fm-git-origin-lib.sh" cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh" cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh" cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh" diff --git a/tests/fm-turnend-guard.test.sh b/tests/fm-turnend-guard.test.sh index 8da2671a405..32b5c7499da 100755 --- a/tests/fm-turnend-guard.test.sh +++ b/tests/fm-turnend-guard.test.sh @@ -193,6 +193,7 @@ install_guard_scripts() { cp "$ROOT/bin/fm-supervision-engine-lib.sh" "$dir/bin/fm-supervision-engine-lib.sh" cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh" cp "$ROOT/bin/fm-path-lib.sh" "$dir/bin/fm-path-lib.sh" + cp "$ROOT/bin/fm-git-origin-lib.sh" "$dir/bin/fm-git-origin-lib.sh" cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh" cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh" cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh" @@ -1214,6 +1215,7 @@ install_integrated_autoarm() { cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh" cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh" cp "$ROOT/bin/fm-path-lib.sh" "$dir/bin/fm-path-lib.sh" + cp "$ROOT/bin/fm-git-origin-lib.sh" "$dir/bin/fm-git-origin-lib.sh" cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh" cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh" cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh"