Skip to content
2 changes: 1 addition & 1 deletion .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <tool> (install: <command>)` - 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 <approved tools...>`.
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.
Expand Down
4 changes: 3 additions & 1 deletion .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ Read the targeted current state with `bin/fm-crew-state.sh <id>` 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 <root>` for treehouse-backed tmux, herdr, zellij, or cmux tasks, where `<root>` 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.
Expand Down
13 changes: 8 additions & 5 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion bin/fm-home-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
# a fresh firstmate worktree via "treehouse get --lease", which durably
# leases the worktree under the secondmate <id> 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
Expand Down
18 changes: 9 additions & 9 deletions bin/fm-install-treehouse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
# Usage:
# fm-install-treehouse.sh <destination-directory>
#
# 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

Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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}") ;;
*)
Expand Down
70 changes: 69 additions & 1 deletion bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>.meta omit them.
# A ship or scout spawn on a Treehouse-backed backend (every backend except orca)
# acquires its slot with `treehouse get --root <root>`, where <root> 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/<id>.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
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -2703,6 +2715,33 @@ spawn_worktree_isolated() { # <path>
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() { # <source> <inspect-target>
local source=$1 inspect_target=$2
if ! spawn_worktree_isolated "$WT"; then
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading