diff --git a/.agents/skills/bootstrap-diagnostics/SKILL.md b/.agents/skills/bootstrap-diagnostics/SKILL.md index 1d49f4b8312..fb2c6c88c7b 100644 --- a/.agents/skills/bootstrap-diagnostics/SKILL.md +++ b/.agents/skills/bootstrap-diagnostics/SKILL.md @@ -17,7 +17,7 @@ The inline rules in `AGENTS.md` section 3 still bind: detect, then consent, then When any diagnostic needs captain attention, report the plain consequence and requested action using `AGENTS.md` section 9's captain-facing translation contract; do not name the diagnostic label unless the captain needs to paste it into a command or issue. - `MISSING: (install: )` - list the missing tools to the captain with a one-line purpose each plus the printed install commands, wait for consent (one approval may cover the list), then run `bin/fm-bootstrap.sh install `. - For `treehouse`, this also covers an installed version whose `treehouse get` lacks `--lease`; treat it as an upgrade request. + For `treehouse`, an installed version that fails the capability check owned by [`bin/fm-bootstrap.sh`](../../../bin/fm-bootstrap.sh) also requires an upgrade; use the printed install command after consent. For `no-mistakes`, this also covers an installed version older than 1.46.0, because this repo's PR gate requires structured pipeline attestation that older builds do not write. For essential axi-family tools - `gh-axi`, `tasks-axi`, `quota-axi` - an installed version below its floor is a plain upgrade request; [`bin/fm-bootstrap.sh`](../../../bin/fm-bootstrap.sh) owns the floor policy, and never argue the floor down to whatever the home happens to have installed. For `tasks-axi`, this additionally covers an installed build that fails the separate feature probe (`bin/fm-tasks-axi-lib.sh` owns the definition); `config/backlog-backend=manual` only suppresses the verbose `BOOTSTRAP_INFO: tasks-axi available` fact, not this missing-tool report. diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index c5209051a44..010bb2cc456 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -32,7 +32,9 @@ Read the targeted current state with `bin/fm-crew-state.sh ` before deciding A no-mistakes run matched to the crew's branch and current code remains authoritative when the endpoint is dead: handle a terminal or parked run through the normal lifecycle, and keep supervising an active run instead of creating a duplicate worker. When no authoritative run accounts for the task, inspect only its recorded backend and worktree inventory. -Use `treehouse status` for treehouse-backed tmux, herdr, zellij, or cmux tasks, and use the recorded `orca_worktree_id=` and `terminal=` for Orca tasks. +Use `treehouse status --root ` for treehouse-backed tmux, herdr, zellij, or cmux tasks, where `` is `FM_TREEHOUSE_TASK_ROOT` from `bin/fm-wake-lib.sh`'s `fm_treehouse_task_root` using the recorded `treehouse_root=` (empty for legacy records) and `worktree=`. +Stop if reconciliation fails; a bare `treehouse status` does not reliably select the task's recorded pool. +Use the recorded `orca_worktree_id=` and `terminal=` for Orca tasks. Do not sweep another home's endpoints or infer ownership from a matching window label. Before relaunch, prove that no live agent still owns the recorded task and that the existing worktree remains available. diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index 1c550c71f10..597efbe86b3 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -54,7 +54,7 @@ # 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. # treehouse is also MISSING when its installed version lacks -# "treehouse get --lease" support. +# "treehouse get --lease" or "--root" support. # no-mistakes is also MISSING when its installed version is older than # 1.46.0 (structured pipeline attestation floor; see CONTRIBUTING.md). # The AXI-family floor policy is owned beside GH_AXI_MIN and @@ -917,8 +917,11 @@ NO_MISTAKES_MIN=1.46.0 GH_AXI_MIN=0.1.29 LAVISH_AXI_MIN=0.1.46 -treehouse_supports_lease() { - treehouse get --help 2>&1 | grep -Eq '(^|[^[:alnum:]_-])--lease([^[:alnum:]_-]|$)' +treehouse_supports_required_flags() { + local help + help=$(treehouse get --help 2>&1) || return 1 + printf '%s\n' "$help" | grep -Eq '(^|[^[:alnum:]_-])--lease([^[:alnum:]_-]|$)' && + printf '%s\n' "$help" | grep -Eq '(^|[^[:alnum:]_-])--root([^[:alnum:]_-]|$)' } # Shared semantic-version floor for the tool gates below. A version string that @@ -1428,11 +1431,11 @@ detect_local_tools() { for t in $COMMON_TOOLS; do command -v "$t" >/dev/null || missing_tool_diagnostic "$t" done - # The treehouse lease-support upgrade check is only relevant when the resolved + # The treehouse capability upgrade check is only relevant when the resolved # backend actually requires treehouse (every backend except orca, which owns its # own worktrees); an orca home must not be told to upgrade a provider it never uses. if fm_backend_list_contains "$TOOLS" treehouse \ - && command -v treehouse >/dev/null 2>&1 && ! treehouse_supports_lease; then + && command -v treehouse >/dev/null 2>&1 && ! treehouse_supports_required_flags; then echo "MISSING: treehouse (install: $(install_cmd treehouse))" fi if command -v no-mistakes >/dev/null 2>&1 && ! tool_version_at_least no-mistakes "$NO_MISTAKES_MIN"; then diff --git a/bin/fm-home-seed.sh b/bin/fm-home-seed.sh index 6693ab1df74..7a2897f9c33 100755 --- a/bin/fm-home-seed.sh +++ b/bin/fm-home-seed.sh @@ -7,7 +7,11 @@ # a fresh firstmate worktree via "treehouse get --lease", which durably # leases the worktree under the secondmate so the home survives with # no live process and is never recycled until the lease is released with -# "treehouse return". Projects are cloned +# "treehouse return". That lease is the PRIMARY's own, taken from the +# firstmate repository's pool using Treehouse's own root resolution, and is +# deliberately not one of the per-home child project pools that +# bin/fm-wake-lib.sh's fm_treehouse_home_root gives ship and scout +# slots, so registered homes keep resolving. Projects are cloned # from the active home into the secondmate home's projects/ directory. # That project list is non-exclusive provisioning data. Pass --no-projects # instead of a project list to seed a project-less home for a domain whose diff --git a/bin/fm-install-treehouse.sh b/bin/fm-install-treehouse.sh index 9d181439c87..2ca0ea49797 100755 --- a/bin/fm-install-treehouse.sh +++ b/bin/fm-install-treehouse.sh @@ -9,12 +9,12 @@ # Usage: # fm-install-treehouse.sh # -# Pins Treehouse v2.0.1, the version exercised by the local real-Herdr suite. +# Pins Treehouse v2.3.0 with durable leases and explicit root selection. set -eu -FM_TREEHOUSE_CI_VERSION=2.0.1 +FM_TREEHOUSE_CI_VERSION=2.3.0 FM_TREEHOUSE_CI_TAG="v${FM_TREEHOUSE_CI_VERSION}" -# Bounded download ceiling (bytes). Official 2.0.1 archives are under 8 MiB. +# Bounded download ceiling (bytes). FM_TREEHOUSE_CI_MAX_BYTES=15000000 FM_TREEHOUSE_CI_REPO=kunchenguid/treehouse @@ -30,19 +30,19 @@ arch=$(uname -m) case "${os}-${arch}" in Linux-x86_64) ARCHIVE=treehouse-v${FM_TREEHOUSE_CI_VERSION}-linux-amd64.tar.gz - SHA256=1d5a32751ab921670103fd201ddb2b91b47338cb13976f45642b827cf8976af2 + SHA256=94fd2b2c20c35aac1ddc2941317890ad82c9916f5ccecbac4a50cda783eed10f ;; Linux-aarch64|Linux-arm64) ARCHIVE=treehouse-v${FM_TREEHOUSE_CI_VERSION}-linux-arm64.tar.gz - SHA256=eaccc9c5b98125df8bd77425598eeecee66cb0371db4eb1cf75f0d813c18fab9 + SHA256=408589ba72b58d5e942071ed863a83fd96566cfd1e514945daa59defde528bbb ;; Darwin-arm64) ARCHIVE=treehouse-v${FM_TREEHOUSE_CI_VERSION}-darwin-arm64.tar.gz - SHA256=7ee5078f3d1f33c01196548797fce65408e459d53530b77d4ba56e074fa1c1a2 + SHA256=1cb09bcfa830b4eec5e54beeaa71589adb9c5d828573dda0f5150e2d80cf13d5 ;; Darwin-x86_64) ARCHIVE=treehouse-v${FM_TREEHOUSE_CI_VERSION}-darwin-amd64.tar.gz - SHA256=1cf44580a5837f995e1d3bb74f4fbd3112b642acd20406087d9735a8106112fd + SHA256=349afcc13c2beb20d846eb560a11b30e1a5cab8e2dfb22988a36aa7f213b5881 ;; *) die "unsupported platform ${os}-${arch}; official Treehouse assets are linux/darwin amd64 and arm64" @@ -68,7 +68,7 @@ fi [ "$ACTUAL_SHA256" = "$SHA256" ] || die "checksum mismatch for $ARCHIVE (expected $SHA256, got $ACTUAL_SHA256)" tar -xzf "$TMP/$ARCHIVE" -C "$TMP" -# Archive layout: a single `treehouse` binary at the archive root (verified for v2.0.1). +# Archive layout: a single `treehouse` binary at the archive root (verified for v2.3.0). if [ -f "$TMP/treehouse" ]; then BIN="$TMP/treehouse" elif [ -f "$TMP/treehouse-v${FM_TREEHOUSE_CI_VERSION}/treehouse" ]; then @@ -82,7 +82,7 @@ mkdir -p "$DESTINATION" install -m 0755 "$BIN" "$DESTINATION/treehouse" installed_version=$("$DESTINATION/treehouse" --version 2>/dev/null | tr -d '[:space:]') -# treehouse prints "v2.0.1" (leading v) on --version. +# treehouse prints "v2.3.0" (leading v) on --version. case "$installed_version" in "v${FM_TREEHOUSE_CI_VERSION}"|"${FM_TREEHOUSE_CI_VERSION}") ;; *) diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 88fa2fcfabe..0f20fd29bbd 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -347,6 +347,17 @@ # A ship task records the explicit mode/yolo it was passed; a secondmate spawn records # mode=secondmate, yolo=off, home=, and projects=; a scout records neither, and both the # success line and state/.meta omit them. +# A ship or scout spawn on a Treehouse-backed backend (every backend except orca) +# acquires its slot with `treehouse get --root `, where is this +# home's own CLI root (bin/fm-wake-lib.sh's fm_treehouse_home_root), and records +# that absolute root as treehouse_root= in state/.meta. A slot outside its pool +# refuses the launch. A relaunch keeps the recorded treehouse_root= line; a record without +# one predates the field and resolves its root from the slot path itself. +# Fresh allocation and relaunch refuse a slot whose owner claim names another +# canonical home with an extant task record, independently of runtime liveness. +# A prior record in the same canonical home does not reserve a reusable slot; +# this check leaves Treehouse's allocation eligibility and teardown's ownership +# proof intact. Missing-home or unreadable claims refuse rather than guess. # Every fresh spawn or relaunch records a new spawn_gen= incarnation token so durable # consumers can distinguish a replacement worker that reuses the same task id. # When the home session's frozen trace-context decision is enabled (see @@ -1046,6 +1057,7 @@ SPAWN_TASK_SET_LOCK_HELD=0 SPAWN_TREEHOUSE_PROJECT_LOCK= SPAWN_TREEHOUSE_PROJECT_LOCK_HELD=0 SPAWN_SLOT_CLAIMED=0 +SPAWN_TREEHOUSE_ROOT= RELAUNCH_REPLACEMENT_PENDING=0 RELAUNCH_REPLACEMENT_BUSY_GEN= RELAUNCH_REPLACEMENT_HARNESS= @@ -2703,6 +2715,33 @@ spawn_worktree_isolated() { # return 0 } +spawn_refuse_live_foreign_claim() { + local worktree=$1 inspect_target=$2 owner_id owner_home owner_meta + fm_treehouse_slot_owner_state "$worktree" "$ID" + case "$FM_TREEHOUSE_SLOT_OWNER" in + absent) return 0 ;; + mine|other) + owner_id=$FM_TREEHOUSE_SLOT_OWNER_ID + owner_home=$FM_TREEHOUSE_SLOT_OWNER_HOME + if [ -z "$owner_home" ]; then + echo "error: Treehouse slot $worktree is claimed by task $owner_id without a home; refusing to launch without knowing which home owns it; inspect window $inspect_target" >&2 + return 1 + fi + if [ "$(real_path_or_raw "$owner_home")" = "$(real_path_or_raw "$FM_HOME")" ]; then + return 0 + fi + owner_meta="$owner_home/state/$owner_id.meta" + if [ -e "$owner_meta" ] || [ -L "$owner_meta" ]; then + echo "error: Treehouse slot $worktree is still held by task $owner_id of foreign home $owner_home (record $owner_meta exists); refusing to launch; inspect window $inspect_target" >&2 + return 1 + fi + return 0 + ;; + esac + echo "error: Treehouse slot $worktree has an unreadable owner claim; refusing to launch without knowing which home owns it; inspect window $inspect_target" >&2 + return 1 +} + validate_spawn_worktree() { # local source=$1 inspect_target=$2 if ! spawn_worktree_isolated "$WT"; then @@ -2946,6 +2985,9 @@ if [ "$RELAUNCH" -eq 1 ]; then # A secondmate's home already resolved WT above through the same validation a # fresh secondmate spawn uses; every other kind takes the recorded worktree. [ "$KIND" = secondmate ] || WT=$RELAUNCH_WT + if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then + spawn_refuse_live_foreign_claim "$WT" "$T" || exit 1 + fi WT_TARGET=$T SES=${T%%:*} else @@ -3464,7 +3506,23 @@ if [ "$RELAUNCH" -eq 1 ]; then fi [ "$KIND" = secondmate ] || validate_spawn_worktree "relaunch" "$T" elif [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then - spawn_send_text_line "$WT_TARGET" 'treehouse get' + # Acquire the slot from THIS home's Treehouse root (bin/fm-wake-lib.sh's + # fm_treehouse_home_root owns the contract). Treehouse names a pool after the + # repository, so without an explicit root two homes holding clones of one + # remote share a single pool and can be handed each other's slots. The root + # is passed as --root, which overrides TREEHOUSE_ROOT and any treehouse.toml + # in the pane's own environment, and is recorded below as treehouse_root= so + # every later call on the slot uses the root it was actually taken from. + SPAWN_TREEHOUSE_ROOT=$(fm_treehouse_home_root "$FM_HOME" "$PROJ_ABS") || { + echo "error: could not resolve this home's Treehouse root for $FM_HOME; refusing to allocate from a pool another home may share" >&2 + exit 1 + } + mkdir -p -- "$SPAWN_TREEHOUSE_ROOT" 2>/dev/null || { + echo "error: could not create this home's Treehouse root $SPAWN_TREEHOUSE_ROOT" >&2 + exit 1 + } + SPAWN_TREEHOUSE_ROOT=$(CDPATH='' cd -- "$SPAWN_TREEHOUSE_ROOT" && pwd -P) || exit 1 + spawn_send_text_line "$WT_TARGET" "treehouse get --root $(shell_quote "$SPAWN_TREEHOUSE_ROOT")" # Wait for the treehouse subshell: the pane's cwd moves from the project to the worktree. # Target the stable window id, not the name: if the name is ever lost (e.g. an @@ -3537,7 +3595,12 @@ elif [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then # under its successor. # Written under the Treehouse project lock held from before slot allocation # through metadata publication, so no other spawn or return sees a half-claim. + spawn_refuse_live_foreign_claim "$WT" "$T" || exit 1 if fm_treehouse_pool_slot "$PROJ_ABS" "$WT"; then + fm_treehouse_task_root "$SPAWN_TREEHOUSE_ROOT" "$WT" || { + echo "error: treehouse get entered $WT outside this home's Treehouse root ($FM_TREEHOUSE_ROOT_REASON); refusing to launch into a slot another home may own; inspect window $T" >&2 + exit 1 + } if ! fm_treehouse_slot_owner_claim "$WT" "$ID" "$FM_HOME"; then echo "error: could not claim Treehouse pool slot $WT for task $ID; refusing to launch a worker whose slot cannot later be proved to be its own; inspect window $T" >&2 exit 1 @@ -4074,6 +4137,11 @@ preserve_relaunch_meta() { echo "endpoint_task_id=$ID" echo "worktree=$WT" echo "project=$PROJ_ABS" + # The Treehouse root the slot was taken from (fm_treehouse_home_root). Written + # only by the fresh spawn that ran `treehouse get`; a relaunch passes the + # recorded line through untouched, and a record without it predates the field + # and resolves its root from the slot's own path (fm_treehouse_task_root). + [ "$RELAUNCH" -eq 1 ] || [ -z "$SPAWN_TREEHOUSE_ROOT" ] || echo "treehouse_root=$SPAWN_TREEHOUSE_ROOT" echo "harness=$HARNESS" echo "kind=$KIND" [ -z "$MODE" ] || echo "mode=$MODE" diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index dcdac9ef2db..b7465b112df 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -161,6 +161,16 @@ # leased home releases its durable treehouse lease so the pool slot is freed, # never left leased forever. If the treehouse return fails, teardown leaves the # leased home and state in place instead of hiding a still-held lease. +# Treehouse root (per-home pools): every `treehouse return` on a task's slot +# passes `--root `, where is reconciled by bin/fm-wake-lib.sh's +# fm_treehouse_task_root between the record's treehouse_root= line and the root +# the slot actually sits under. A record that predates the field (no +# treehouse_root=) resolves to the slot's own root, so legacy slots drain through +# the pool that allocated them with no migration; a record whose treehouse_root= +# does not contain its worktree REFUSES before any mutation, because returning +# through the wrong root would act on another home's slot. A secondmate home is +# the primary's own durable lease from the firstmate repository's pool and is +# returned without --root, exactly as it was leased. # Usage: fm-teardown.sh [--force] [--legacy-record] # --force skips ordinary-task dirty and landed-work checks, skips scout report # checks, and discards secondmate child work for kind=secondmate. Only use it @@ -1008,6 +1018,17 @@ elif [ "$TREEHOUSE_SLOT_LOCK_REQUIRED" = 1 ]; then echo "REFUSED: task $ID stopped naming a live Treehouse slot while teardown acquired its locks; nothing was changed" >&2 exit 1 fi +# The root every Treehouse call on this task's slot uses (script header, +# "Treehouse root"): the recorded treehouse_root= when it contains the slot, the +# slot's own root for a record that predates the field, and a refusal otherwise. +TEARDOWN_TREEHOUSE_ROOT= +if [ -n "$EXPECTED_TREEHOUSE_PROJECT_LOCK" ]; then + fm_treehouse_task_root "$(fm_meta_get "$META" treehouse_root)" "$WT" || { + echo "REFUSED: task $ID's worktree $WT is not under its recorded Treehouse root ($FM_TREEHOUSE_ROOT_REASON); returning it through that root would act on another home's slot, so nothing was changed - not even with --force." >&2 + exit 1 + } + TEARDOWN_TREEHOUSE_ROOT=$FM_TREEHOUSE_TASK_ROOT +fi MODE=$(grep '^mode=' "$META" | cut -d= -f2- || true) [ -n "$MODE" ] || MODE=no-mistakes @@ -1635,13 +1656,18 @@ cleanup_stale_lock_for_safety_check() { # Return a worktree/home via `treehouse return --force`, tolerating a transient or # stale git index.lock left by a killed crew process. See the script header. -teardown_treehouse_return() { - local dir=$1 cd_dir=$2 label=$3 post_cleanup_check=${4:-} +# A nonempty is passed as `--root `, so the return acts on the pool +# the slot was taken from (script header, "Treehouse root"); an empty +# leaves the pool to Treehouse's own resolution, as a secondmate home lease needs. +teardown_treehouse_return() { #