From 80809ebc9dfd6d30048a5480ef21596c9aa27c25 Mon Sep 17 00:00:00 2001 From: Hezki Date: Sun, 4 Oct 2026 23:38:18 +1100 Subject: [PATCH 1/4] fix(bin): trust a pooled worktree of another clone of the same origin Treehouse shares one pool among every clone of an origin, so a claude spawn from a secondmate home could be handed a slot of another home's clone and fm-claude-trust refused it. Accept a pooled same-origin worktree, keep every existing refusal, and let fm_treehouse_pool_slot recognize such slots too. --- bin/fm-claude-trust.sh | 41 ++++++++++++++---- bin/fm-git-origin-lib.sh | 36 ++++++++++++++++ bin/fm-wake-lib.sh | 22 +++++----- docs/scripts.md | 1 + tests/fm-claude-trust.test.sh | 79 +++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 bin/fm-git-origin-lib.sh diff --git a/bin/fm-claude-trust.sh b/bin/fm-claude-trust.sh index 8fc3fb6b88a..9fe62a9e6ab 100755 --- a/bin/fm-claude-trust.sh +++ b/bin/fm-claude-trust.sh @@ -228,6 +228,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,7 +300,24 @@ 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 @@ -313,19 +332,25 @@ if [ "$MODE" = worktree ]; then # assumed: the candidate's own resolved git dir must equal PROJ_COMMON, 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-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/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-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 From 2a4292f48d6feef69d9933e8612312e40930d454 Mon Sep 17 00:00:00 2001 From: Hezki Date: Sun, 4 Oct 2026 23:43:45 +1100 Subject: [PATCH 2/4] no-mistakes(document): Document cross-clone Treehouse slot acceptance in Claude trust --- bin/fm-claude-trust.sh | 9 ++++++--- docs/orca-backend.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/fm-claude-trust.sh b/bin/fm-claude-trust.sh index 9fe62a9e6ab..84389d298f7 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 @@ -89,8 +90,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 diff --git a/docs/orca-backend.md b/docs/orca-backend.md index 000782a3536..0978dce60bf 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 in `bin/fm-claude-trust.sh`, which refuses any path that is not a linked git worktree sharing the project's git common dir (apart from a Treehouse pool slot of a same-origin clone), so a claude spawn on Orca fails loudly at that check rather than launching if Orca clones instead of linking. ## Regression entry points From 00bb207bc51958e8ebdc16a829b7bb69ac7add39 Mon Sep 17 00:00:00 2001 From: Hezki Date: Mon, 5 Oct 2026 00:31:37 +1100 Subject: [PATCH 3/4] no-mistakes(ci): Root cause: this PR made bin/fm-wake-lib.sh source a new library, bin/fm-git-origin-lib.sh. Every place that ships the wake lib together with its dependencies (fm-wake-lib.sh plus fm-path-lib.sh) never got the new file. Sourcing therefore failed with "fm-git-origin-lib.sh: No such file or directory", which broke the failing tests in shards 2, 3, 4, 6 and 8. Invariant: any set of files that ships fm-wake-lib.sh must also ship every library it sources. I found every such set by grepping for fm-path-lib.sh, and fixed each one the same way, by adding fm-git-origin-lib.sh next to fm-path-lib.sh: - bin/fm-teardown.sh: its required-source preflight list. - 12 test files, in their fixture copy/link lists: fm-turnend-guard (2 sites), fm-claude-stop-autoarm, fm-session-lock-ancestry, fm-afk-return, fm-mail-check, fm-gotmp (2 sites), fm-remote-transport-lanes, fm-remote-backlog-handoff, fm-pi-branch-extension, fm-cursor-primary, fm-extension-binding. I did not change the trust or wake logic. Verified locally, all passing (exit 0, no "not ok"): fm-turnend-guard, fm-claude-stop-autoarm, fm-afk-return, fm-gotmp, fm-remote-backlog-handoff, fm-remote-transport-lanes, fm-pi-branch-extension, fm-calm-pi-extension, fm-extension-binding, fm-mail-check, fm-cursor-primary, fm-claude-trust and fm-teardown-endpoint-safety. That covers every test file that failed in CI. fm-extension-binding finished in only 5 seconds, and I did not check whether it skipped part of its run. bin/fm-lint.sh also passes with ShellCheck 0.11.0 and actionlint 1.7.12. fm-session-lock-ancestry fails locally with "the pty-host was not reparented to init", but it fails the same way on the unmodified tree, so that is this local environment, not this change. It did not fail in CI --- bin/fm-teardown.sh | 1 + tests/fm-afk-return.test.sh | 1 + tests/fm-claude-stop-autoarm.test.sh | 1 + tests/fm-cursor-primary.test.sh | 2 +- tests/fm-gotmp.test.sh | 2 ++ tests/fm-mail-check.test.sh | 1 + tests/fm-pi-branch-extension.test.sh | 3 ++- tests/fm-remote-backlog-handoff.test.sh | 3 ++- tests/fm-remote-transport-lanes.test.sh | 3 ++- tests/fm-session-lock-ancestry.test.sh | 1 + tests/fm-turnend-guard.test.sh | 2 ++ 11 files changed, 16 insertions(+), 4 deletions(-) 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/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-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 fd651cefa01..c7c2a5b0ea0 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" From 4751334d13b435b070b64bf3efbe7ffe51f4700e Mon Sep 17 00:00:00 2001 From: Hezki Date: Mon, 5 Oct 2026 09:18:54 +1100 Subject: [PATCH 4/4] no-mistakes(document): Correct pooled Claude trust documentation --- bin/fm-claude-trust.sh | 15 ++++++++------- docs/orca-backend.md | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bin/fm-claude-trust.sh b/bin/fm-claude-trust.sh index 84389d298f7..833bcf0aa16 100755 --- a/bin/fm-claude-trust.sh +++ b/bin/fm-claude-trust.sh @@ -52,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 @@ -327,12 +328,12 @@ if [ "$MODE" = worktree ]; then # 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. if [ "$CROSS_CLONE" = 1 ]; then diff --git a/docs/orca-backend.md b/docs/orca-backend.md index 0978dce60bf..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 (apart from a Treehouse pool slot of a same-origin clone), 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