diff --git a/.agents/skills/bootstrap-diagnostics/SKILL.md b/.agents/skills/bootstrap-diagnostics/SKILL.md index 95932444f8..23bf37d178 100644 --- a/.agents/skills/bootstrap-diagnostics/SKILL.md +++ b/.agents/skills/bootstrap-diagnostics/SKILL.md @@ -2,7 +2,7 @@ name: bootstrap-diagnostics description: >- Agent-only handling playbook for session-start bootstrap diagnostics. - Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, STARTUP_MEMORY_BUDGET, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, PR_CHECK_MIGRATION, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines. + Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, LANDING_REMOTE, STARTUP_MEMORY_BUDGET, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, PR_CHECK_MIGRATION, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines. A silent bootstrap section, or a BOOTSTRAP_INFO fact, means no skill load. user-invocable: false metadata: @@ -32,6 +32,9 @@ When any diagnostic needs captain attention, report the plain consequence and re - `TANGLE: ` - the primary checkout is stranded on a feature branch instead of its default branch; `AGENTS.md` section 8 explains why this guard exists and what it protects. The work is safe on that branch ref; restore the primary to its default branch with the printed `git -C checkout `, then re-validate that branch in a proper worktree. This is the only sanctioned firstmate-initiated git write to the primary, and it is a non-destructive branch switch that strands nothing. +- `LANDING_REMOTE: ; ` - the primary checkout's remotes no longer have the shape the landing remap leaves behind, so a branch, a push, or a PR can go to the third-party parent instead of the repository work lands on. + The line names the one broken invariant and the command that repairs it; `docs/configuration.md`'s "Landing remote" section owns why each invariant matters. + The repair rewrites the primary's remotes, so run it on the primary checkout and never from a linked worktree, and report the consequence to the captain before dispatching work whose PR would land in the wrong place. - `STARTUP_MEMORY_BUDGET: invalid config/startup-memory-budget - ` - the visible startup-memory budget is not a safe one-line positive decimal file; do not infer the default or propagate it. Correct the local primary file, then rerun session start so the normal convergence path can deliver the validated value to secondmate homes. - `CREW_DISPATCH: invalid config/crew-dispatch.json - ` - the optional dispatch profile file exists but failed low-cost bootstrap validation; stop profile-based dispatch, report the actionable error, and require correction of the malformed schema, unverified harness name, or invalid harness/effort pair rather than falling back around it or selecting a bad profile. diff --git a/.opencode/plugins/fm-primary-watch-arm.js b/.opencode/plugins/fm-primary-watch-arm.js index e88c248f78..d459c1c929 100644 --- a/.opencode/plugins/fm-primary-watch-arm.js +++ b/.opencode/plugins/fm-primary-watch-arm.js @@ -441,7 +441,7 @@ export const FmPrimaryWatchArm = async ({ client, directory, worktree }) => { if (event.type !== "session.idle") return; const sessionID = event.properties?.sessionID; if (!sessionID) return; - void ensureArm(paths, sessionID, client); + return ensureArm(paths, sessionID, client); }, }; }; diff --git a/AGENTS.md b/AGENTS.md index d021652b35..927693d3e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -528,7 +528,7 @@ It updates cloned repositories and firstmate from origin using local patch stack These skills are not captain-invocable; load them only at their precise triggers. -- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `PR_CHECK_MIGRATION:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`); silence and `BOOTSTRAP_INFO:` need no load. +- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `LANDING_REMOTE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `PR_CHECK_MIGRATION:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`); silence and `BOOTSTRAP_INFO:` need no load. - `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report. - `ask-user-authority` - load before deciding any ask-user finding, regardless of the project's `yolo` posture. - `quota-array-dispatch` - load before choosing among a matched crew-dispatch profile array from current quota-axi output. diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index 725d769151..a42ca81241 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -13,6 +13,7 @@ # "FLEET_SYNC: : skipped|recovered|STUCK: ", # "PR_CHECK_MIGRATION: ", # "TANGLE: ", +# "LANDING_REMOTE: ; ", # "SECONDMATE_SYNC: secondmate : skipped: ", # "NUDGE_SECONDMATES: secondmate : send failed: ", # "BOOTSTRAP_INFO: nudged fm- with ''", @@ -48,6 +49,12 @@ # A TANGLE line means the firstmate primary checkout (FM_ROOT) is stranded # on a feature branch instead of its default branch - a crewmate's work # landed in the primary instead of its own worktree; restore it per the line. +# A LANDING_REMOTE line means the primary's remotes no longer have the +# shape bin/fm-landing-remote.sh apply leaves behind, so git, gh, or +# no-mistakes could send a branch, a push, or a PR to the third-party +# parent. That check is bin/fm-landing-remote.sh verify with no --ours, +# it needs no network, and it is silent for a single-remote clone that +# never had a parent to be remapped away from. # treehouse is also MISSING when its installed version lacks # "treehouse get --lease" support. # no-mistakes is also MISSING when its installed version is older than @@ -83,8 +90,8 @@ # (PR-check migration, secondmate_sync, secondmate_liveness_sweep, # secondmate_handoff_resume, x_mode_setup, fleet_sync) while still # printing every read-only detect line -# above; the TANGLE line switches to advisory-only wording with no -# checkout command. Used by +# above; the TANGLE and LANDING_REMOTE lines switch to advisory-only +# wording with no checkout or remap command. Used by # fm-session-start.sh's read-only path when another live session holds # the fleet lock, so a second concurrent session never race-mutates # PR-check artifacts, secondmate homes, pending handoff outboxes, @@ -116,8 +123,9 @@ # because THIS session already ran them while holding the fleet lock, # rather than because it has no lock at all. The two cases differ in # exactly one place: repair ownership. A locked session is told to -# restore a tangled primary checkout itself, while an unlocked one is -# told to leave that work to the lock holder. Unset/0 (the default) +# restore a tangled primary checkout or remap drifted landing remotes +# itself, while an unlocked one is told to leave that work to the lock +# holder. Unset/0 (the default) # keeps detect-only meaning unlocked, exactly as before. # fm-bootstrap.sh install ... # Install the named tools (only ones the captain approved). @@ -1173,6 +1181,28 @@ detect_local_config() { echo "TANGLE: primary checkout on feature branch '$tangle_branch' (expected '$tangle_default'); the work is safe on that ref - restore the primary with: git -C $FM_ROOT checkout $tangle_default, then re-validate the branch in a proper worktree" fi fi + # Landing-remote drift: apply is a one-off the operator runs on the primary, + # so nothing re-asserts it afterwards. Surface a primary whose remotes drifted + # back toward the parent - or that was never remapped at all - here, where the + # session already reads every other local hazard. bin/fm-landing-remote.sh is + # the one owner of the shape; this only relays its refusal. + # Silent for a root that is not a git checkout at all, exactly as the tangle + # check above is: that is a different problem and this check has nothing to say + # about it. + if git -C "$FM_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + landing_drift=$("$SCRIPT_DIR/fm-landing-remote.sh" verify --repo "$FM_ROOT" 2>&1 >/dev/null) || { + landing_drift=${landing_drift#error: } + landing_drift=${landing_drift%%$'\n'*} + if [ -n "$landing_drift" ]; then + if [ "${FM_BOOTSTRAP_DETECT_ONLY:-0}" = 1 ] && [ "${FM_BOOTSTRAP_LOCKED:-0}" != 1 ]; then + landing_drift_cause=${landing_drift%%; *} + echo "LANDING_REMOTE: $landing_drift_cause; read-only session must leave remap work to the session holding the fleet lock" + else + echo "LANDING_REMOTE: $landing_drift" + fi + fi + } + fi crew= [ -f "$CONFIG/crew-harness" ] && crew=$(tr -d '[:space:]' < "$CONFIG/crew-harness" || true) if [ "${FM_BOOTSTRAP_VERBOSE_FACTS:-0}" = 1 ] && [ -n "$crew" ] && [ "$crew" != "default" ]; then diff --git a/bin/fm-ff-lib.sh b/bin/fm-ff-lib.sh index 77d87cf6a9..0788b6b52d 100644 --- a/bin/fm-ff-lib.sh +++ b/bin/fm-ff-lib.sh @@ -42,7 +42,7 @@ default_branch() { return 0 fi for branch in main master; do - if git -C "$dir" show-ref --verify --quiet "refs/heads/$branch"; then + if git -C "$dir" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then echo "$branch" return 0 fi diff --git a/bin/fm-fleet-sync.sh b/bin/fm-fleet-sync.sh index 396b9f2edc..c8fd1e93e1 100755 --- a/bin/fm-fleet-sync.sh +++ b/bin/fm-fleet-sync.sh @@ -38,6 +38,8 @@ PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}" # Inert unless FM_TIMING_LOG names a file; only the deferred network stage sets it. # shellcheck source=bin/fm-timing-lib.sh . "$SCRIPT_DIR/fm-timing-lib.sh" +# shellcheck source=bin/fm-self-repo-lib.sh +. "$SCRIPT_DIR/fm-self-repo-lib.sh" FM_LOCK_LOG_PREFIX=fleet-sync "$FM_ROOT/bin/fm-guard.sh" || true @@ -118,7 +120,7 @@ default_branch() { return 0 fi for branch in main master; do - if git -C "$PROJ" show-ref --verify --quiet "refs/heads/$branch"; then + if git -C "$PROJ" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then echo "$branch" return 0 fi @@ -304,10 +306,7 @@ sync_project() { echo "$label: skipped: not a git repo" return 0 fi - proj_real=$(cd "$PROJ" 2>/dev/null && pwd -P || printf '%s\n' "$PROJ") - root_real=$(cd "$FM_ROOT" 2>/dev/null && pwd -P || printf '%s\n' "$FM_ROOT") - home_real=$(cd "$FM_HOME" 2>/dev/null && pwd -P || printf '%s\n' "$FM_HOME") - if [ "$proj_real" = "$root_real" ] || [ "$proj_real" = "$home_real" ]; then + if fm_is_firstmate_repo "$PROJ" "$FM_ROOT" "$FM_HOME"; then echo "$label: skipped: firstmate home (upstream sync is manual)" return 0 fi diff --git a/bin/fm-landing-remote.sh b/bin/fm-landing-remote.sh index 588ec37dc0..6090ff4279 100755 --- a/bin/fm-landing-remote.sh +++ b/bin/fm-landing-remote.sh @@ -11,7 +11,7 @@ # Usage: # fm-landing-remote.sh apply --ours --upstream [--repo ] # fm-landing-remote.sh status [--repo ] -# fm-landing-remote.sh verify --ours [--repo ] +# fm-landing-remote.sh verify [--ours ] [--repo ] # # apply rewrites remotes in the named checkout so `origin` is --ours and # `upstream` is --upstream, refetches `origin` on every path that decides to @@ -48,8 +48,23 @@ # status prints the current origin, upstream, leftover fork remote, and the gh # default gh recorded locally in `remote.origin.gh-resolved`; it needs no # network. -# verify exits 0 only when origin matches --ours; otherwise it exits 1 and -# names the remote it actually found. +# verify has two modes and needs no network in either. +# With --ours it is an identity check: it exits 0 only when origin matches that +# URL, and otherwise exits 1 naming the remote it actually found. +# Without --ours it is the runtime drift check, for callers that know a checkout +# must have been remapped but do not carry the landing URL - bin/fm-bootstrap.sh +# runs it against the firstmate primary at every session start. It asserts the +# shape apply guarantees, without needing to know which repository is ours: +# `upstream` present and distinct from `origin`, no leftover `fork` remote, both +# git defaults pointed at `origin`, and gh's recorded default on `origin` when gh +# is installed. Applicability is decided by the checkout, not by a flag: a clone +# with neither `upstream` nor `fork` was never a fork checkout, so the remap does +# not apply to it and the check passes silently. A `fork` remote is the exact +# pre-apply shape and apply always removes it, so its presence means origin may +# still be the parent. Each refusal names the one broken invariant on a single +# stderr line so a caller can embed it in its own diagnostic, and closes with an +# apply command carrying the --ours and --upstream URLs the checkout itself +# states, so the printed repair runs as printed. # # URL comparison is spelling-tolerant for the GitHub https / ssh / trailing # .git forms. apply is a no-op, and touches the network not at all, once origin, @@ -406,10 +421,73 @@ cmd_status() { printf 'gh-default=%s\n' "${gh_default:-absent}" } +# Every refusal below is relayed verbatim by bin/fm-bootstrap.sh as the +# LANDING_REMOTE remediation, so the repair it names has to run as printed. +# apply takes both URLs, so fill them from what the checkout already states and +# fall back to a named placeholder only for a URL the checkout cannot supply. +landing_remote_repair_command() { + local ours=$1 parent=$2 restore_from=${3:-} + if [ -n "$restore_from" ]; then + printf 'run git remote rename %s origin, then fm-landing-remote.sh apply --ours %s --upstream %s on the primary checkout' \ + "$restore_from" "${ours:-}" "${parent:-}" + else + printf 'run fm-landing-remote.sh apply --ours %s --upstream %s on the primary checkout' \ + "${ours:-}" "${parent:-}" + fi +} + +# The invariants apply leaves behind, expressed without the landing URL. Reading +# only what the checkout already states keeps this runnable from any caller that +# knows a remap was owed but does not carry --ours, and keeps it offline. +verify_remapped_shape() { + local origin upstream fork_url fork_parent + + origin=$(remote_url origin) + upstream=$(remote_url upstream) + fork_url=$(remote_url fork) + + if [ -z "$upstream" ] && [ -z "$fork_url" ]; then + # Single-remote clone: nothing was ever forked here, so there is no remap to + # have run and no drift to report. + printf 'origin=%s\n' "${origin:-absent}" + return 0 + fi + if [ -z "$origin" ]; then + local restore_from + if [ -n "$fork_url" ]; then + restore_from="fork" + else + restore_from="upstream" + fi + fail "origin remote is absent, so nothing names the repository work lands on; $(landing_remote_repair_command "$fork_url" "$upstream" "$restore_from")" + fi + if [ -n "$fork_url" ]; then + fork_parent=${upstream:-$origin} + fail "a fork remote at $fork_url still exists beside origin $origin, which is the shape apply exists to remove, so origin may still be the third-party parent; $(landing_remote_repair_command "$fork_url" "$fork_parent")" + fi + if urls_equal "$origin" "$upstream"; then + fail "origin and upstream both name $origin, so work would branch from and open PRs on the third-party parent; $(landing_remote_repair_command "" "$origin")" + fi + if [ "$(git_config_get_local checkout.defaultRemote)" != origin ]; then + fail "checkout.defaultRemote is not origin while upstream $upstream exists, so an ambiguous branch name can resolve against the parent; $(landing_remote_repair_command "$origin" "$upstream")" + fi + if [ "$(git_config_get_local remote.pushDefault)" != origin ]; then + fail "remote.pushDefault is not origin while upstream $upstream exists, so a push can land on the parent; $(landing_remote_repair_command "$origin" "$upstream")" + fi + if ! gh_default_is_origin; then + fail "gh has no recorded default of origin while upstream $upstream exists, and gh ranks upstream above origin, so a flagless 'gh pr create' can open the PR on the parent; $(landing_remote_repair_command "$origin" "$upstream")" + fi + printf 'origin=%s\n' "$origin" + printf 'upstream=%s\n' "$upstream" +} + cmd_verify() { local origin - [ -n "$OURS" ] || fail "verify requires --ours " require_repo + if [ -z "$OURS" ]; then + verify_remapped_shape + return 0 + fi origin=$(remote_url origin) [ -n "$origin" ] || fail "origin remote is absent" if urls_equal "$origin" "$OURS"; then diff --git a/bin/fm-merge-local.sh b/bin/fm-merge-local.sh index 97e300ef74..44812d400f 100755 --- a/bin/fm-merge-local.sh +++ b/bin/fm-merge-local.sh @@ -16,6 +16,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}" FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" +# shellcheck source=bin/fm-self-repo-lib.sh +. "$SCRIPT_DIR/fm-self-repo-lib.sh" "$FM_ROOT/bin/fm-guard.sh" || true ID=${1:?usage: fm-merge-local.sh } META="$STATE/$ID.meta" @@ -24,20 +26,7 @@ META="$STATE/$ID.meta" PROJ=$(grep '^project=' "$META" | cut -d= -f2-) MODE=$(grep '^mode=' "$META" | cut -d= -f2- || true) -canonical_dir() { - local target=$1 - ( cd "$target" 2>/dev/null && pwd -P ) || printf '%s\n' "$target" -} - -is_firstmate_repo() { - local proj_real root_real home_real - proj_real=$(canonical_dir "$PROJ") - root_real=$(canonical_dir "$FM_ROOT") - home_real=$(canonical_dir "$FM_HOME") - [ "$proj_real" = "$root_real" ] || [ "$proj_real" = "$home_real" ] -} - -if [ "$MODE" != "local-only" ] && ! is_firstmate_repo; then +if [ "$MODE" != "local-only" ] && ! fm_is_firstmate_repo "$PROJ" "$FM_ROOT" "$FM_HOME"; then echo "error: task $ID is mode=$MODE on $PROJ, not local-only; merge PR tasks with bin/fm-pr-merge.sh after approval" >&2 exit 1 fi diff --git a/bin/fm-pr-merge.sh b/bin/fm-pr-merge.sh index 3a33d1a7d6..c052b49fb3 100755 --- a/bin/fm-pr-merge.sh +++ b/bin/fm-pr-merge.sh @@ -17,6 +17,8 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" # shellcheck source=bin/fm-pr-lib.sh . "$SCRIPT_DIR/fm-pr-lib.sh" +# shellcheck source=bin/fm-self-repo-lib.sh +. "$SCRIPT_DIR/fm-self-repo-lib.sh" if [ "$#" -lt 2 ]; then echo "error: invalid PR merge request" >&2 @@ -84,11 +86,6 @@ fi gh-axi pr merge "$PR_NUMBER" --repo "$PR_OWNER/$PR_REPO" "${merge_args[@]+"${merge_args[@]}"}" "$@" PROJ=$(grep '^project=' "$META" | cut -d= -f2- || true) -if [ -n "$PROJ" ] && [ -d "$PROJ" ]; then - proj_real=$(cd "$PROJ" 2>/dev/null && pwd -P || printf '%s\n' "$PROJ") - root_real=$(cd "$FM_ROOT" 2>/dev/null && pwd -P || printf '%s\n' "$FM_ROOT") - home_real=$(cd "$FM_HOME" 2>/dev/null && pwd -P || printf '%s\n' "$FM_HOME") - if [ "$proj_real" = "$root_real" ] || [ "$proj_real" = "$home_real" ]; then - "$SCRIPT_DIR/fm-merge-local.sh" "$ID" - fi +if [ -n "$PROJ" ] && [ -d "$PROJ" ] && fm_is_firstmate_repo "$PROJ" "$FM_ROOT" "$FM_HOME"; then + "$SCRIPT_DIR/fm-merge-local.sh" "$ID" fi diff --git a/bin/fm-self-repo-lib.sh b/bin/fm-self-repo-lib.sh new file mode 100644 index 0000000000..c2c9d501ab --- /dev/null +++ b/bin/fm-self-repo-lib.sh @@ -0,0 +1,39 @@ +# shellcheck shell=bash +# Shared "is this project directory firstmate's own repository?" predicate. +# Usage: . bin/fm-self-repo-lib.sh +# +# Firstmate ships work on itself, so its tracked code root (FM_ROOT) and its +# operational home (FM_HOME) both turn up as a task's project directory. Four +# decisions branch on that fact and must agree exactly: +# bin/fm-merge-local.sh accepts a PR-mode task for the local fast-forward +# bin/fm-pr-merge.sh follows a merged PR with that same local landing +# bin/fm-fleet-sync.sh leaves the checkout alone (upstream sync is manual) +# bin/fm-spawn.sh refreshes a task worktree from the LOCAL default +# branch instead of fetching origin +# A project that counts as firstmate for one of them and not another is how a +# worker gets reset onto a remote tip the fleet never reviewed, or how a merged +# firstmate PR silently fails to reach the running tree. One predicate here +# keeps a later fix from reaching three call sites and missing the fourth. +# +# Comparison is by resolved PHYSICAL path, so a symlinked home, a trailing +# slash, or a `..` segment cannot make one directory look like two. A path that +# cannot be resolved keeps its literal spelling rather than collapsing to the +# empty string, which would make two different unresolvable paths compare equal. + +# Echo the physical path of , or the input verbatim when it cannot be +# resolved. +fm_canonical_dir() { # + local target=${1-} + ( cd "$target" 2>/dev/null && pwd -P ) || printf '%s\n' "$target" +} + +# Return 0 when is firstmate's own tracked code root or its +# operational home. An empty project directory is never firstmate's own repo. +fm_is_firstmate_repo() { # + local proj_real root_real home_real + [ -n "${1-}" ] || return 1 + proj_real=$(fm_canonical_dir "$1") + root_real=$(fm_canonical_dir "${2-}") + home_real=$(fm_canonical_dir "${3-}") + [ "$proj_real" = "$root_real" ] || [ "$proj_real" = "$home_real" ] +} diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 1e303097e4..685c133c22 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -271,6 +271,8 @@ SUB_HOME_MARKER=".fm-secondmate-home" . "$SCRIPT_DIR/fm-trace-context-lib.sh" # shellcheck source=bin/fm-remote-readiness-lib.sh . "$SCRIPT_DIR/fm-remote-readiness-lib.sh" +# shellcheck source=bin/fm-self-repo-lib.sh +. "$SCRIPT_DIR/fm-self-repo-lib.sh" # Fail closed before any fleet mutation: a no-mistakes gate agent must never spawn # a direct report (see bin/fm-gate-refuse-lib.sh). fm_refuse_if_gate_agent @@ -1749,16 +1751,7 @@ BRIEF_REAL="$BRIEF_DIR_REAL/$(basename "$BRIEF")" # isolation guard refuses a spawn that never actually tangled). Canonicalize # once here so every downstream comparison uses the same physical form # (docs/herdr-backend.md "Known gaps"). -PROJ_ABS_REAL=$(cd "$PROJ_ABS" 2>/dev/null && pwd -P) || PROJ_ABS_REAL="$PROJ_ABS" - -real_path_or_raw() { # - local path=$1 real - if real=$(cd "$path" 2>/dev/null && pwd -P); then - printf '%s\n' "$real" - else - printf '%s\n' "$path" - fi -} +PROJ_ABS_REAL=$(fm_canonical_dir "$PROJ_ABS") # Session-provider container-ensure + task creation. tmux stays exactly as P1 # left it (same session-name / new-window sequence, see bin/backends/tmux.sh); @@ -1769,18 +1762,17 @@ real_path_or_raw() { # # that every downstream operation (send/capture/kill) already treats as opaque # per-backend routing (fm_backend_resolve_selector). validate_spawn_worktree() { # - local source=$1 inspect_target=$2 wt_real proj_real wt_top wt_top_real + local source=$1 inspect_target=$2 wt_real wt_top wt_top_real wt_real= if ! wt_real=$(cd "$WT" 2>/dev/null && pwd -P); then wt_real= fi - proj_real=$PROJ_ABS_REAL wt_top=$(git -C "$WT" rev-parse --show-toplevel 2>/dev/null || true) wt_top_real= if ! wt_top_real=$(cd "$wt_top" 2>/dev/null && pwd -P); then wt_top_real= fi - if [ -z "$wt_real" ] || [ -z "$wt_top_real" ] || [ "$wt_real" != "$wt_top_real" ] || [ "$wt_real" = "$proj_real" ]; then + if [ -z "$wt_real" ] || [ -z "$wt_top_real" ] || [ "$wt_real" != "$wt_top_real" ] || [ "$wt_real" = "$PROJ_ABS_REAL" ]; then echo "error: $source did not yield an isolated worktree (resolved '$WT'; worktree root '${wt_top:-none}'; primary '$PROJ_ABS'); refusing to launch to avoid tangling the primary checkout. Inspect target $inspect_target" >&2 exit 1 fi @@ -1813,15 +1805,11 @@ reset_spawn_worktree_to_base() { #