Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`, this also covers an installed version below the durable-lease-identity floor that [`docs/configuration.md`](../../../docs/configuration.md#toolchain) states; treat it as an upgrade request.
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
10 changes: 6 additions & 4 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -297,17 +297,19 @@ fm_backend_validate_spawn() { # <name>
# single owner of the per-backend dependency delta, so bootstrap follows the
# RESOLVED backend instead of demanding an inactive backend's tools. Each set is:
# - the session-provider CLI itself (tmux/herdr/zellij/orca/cmux);
# - jq, for the JSON-emitting adapters (herdr, zellij, cmux) whose spawn/liveness
# paths parse the backend's JSON output (see each adapter's
# tool check, e.g. fm_backend_herdr_tool_check);
# - jq, for the JSON-emitting adapters (herdr, zellij, cmux) whose
# spawn/liveness paths parse the backend's JSON output (see each adapter's
# tool check, e.g. fm_backend_herdr_tool_check), and for every treehouse
# backend, whose slot-ownership proof reads Treehouse's JSON lease state
# (bin/fm-wake-lib.sh);
# - the treehouse worktree provider for every session-provider-only backend
# (tmux, herdr, zellij, cmux); orca owns its own task worktree and terminal,
# so it drops both treehouse and any other backend's session CLI.
# Prints a single space-separated line and returns 0 for a known backend; returns
# 1 and prints nothing for an unknown backend.
fm_backend_required_tools() { # <backend>
case "$1" in
tmux) printf '%s' 'tmux treehouse' ;;
tmux) printf '%s' 'tmux jq treehouse' ;;
herdr) printf '%s' 'herdr jq treehouse' ;;
zellij) printf '%s' 'zellij jq treehouse' ;;
cmux) printf '%s' 'cmux jq treehouse' ;;
Expand Down
10 changes: 7 additions & 3 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@
# 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.
# treehouse is also MISSING when its installed version lacks
# "treehouse get --lease" support.
# treehouse is also MISSING when its installed version lacks the
# durable lease identities of Treehouse v2.1.0 or newer
# ("treehouse return --if-lease-id").
# 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 @@ -925,8 +926,11 @@ NO_MISTAKES_MIN=1.46.0
GH_AXI_MIN=0.1.29
LAVISH_AXI_MIN=0.1.46

# Slot ownership needs Treehouse v2.1.0's durable lease identities: lease_id,
# `status --json`, and `return --if-lease-holder`/`--if-lease-id` all arrived
# together, so `return --help` advertising --if-lease-id is the capability probe.
treehouse_supports_lease() {
treehouse get --help 2>&1 | grep -Eq '(^|[^[:alnum:]_-])--lease([^[:alnum:]_-]|$)'
treehouse return --help 2>&1 | grep -Eq '(^|[^[:alnum:]_-])--if-lease-id([^[:alnum:]_-]|$)'
}

# Shared semantic-version floor for the tool gates below. A version string that
Expand Down
20 changes: 19 additions & 1 deletion bin/fm-home-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ SUB_HOME_PARENT_MARKER=".fm-secondmate-parent"
. "$SCRIPT_DIR/fm-secondmate-charter-lib.sh"
# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-backend.sh
. "$SCRIPT_DIR/fm-backend.sh"

usage() {
echo "usage: fm-home-seed.sh <id> <home|-> {<project>...|--no-projects}" >&2
Expand Down Expand Up @@ -388,15 +390,31 @@ seeded_origin_url() {
}

acquire_treehouse_home() {
local id=$1 home
local id=$1 home lock
# Durably lease a firstmate worktree from the pool. The lease persists with no
# live process and is skipped by later get/prune, so the home survives restarts
# until teardown or rollback returns it. treehouse prints only the worktree path
# to stdout (banners go to stderr), so command substitution captures the path.
# The get shares bin/fm-spawn.sh's Treehouse project lock and legacy-record
# preflight, so it never reissues a Firstmate slot a task record still names.
lock=$(fm_treehouse_project_lock_path "$FM_ROOT") || {
echo "error: could not resolve the shared Treehouse project lock for $FM_ROOT" >&2
return 1
}
fm_lock_try_acquire "$lock" || {
echo "error: another Treehouse slot allocation or return is in progress for $FM_ROOT; refusing to race it" >&2
return 1
}
if ! fm_treehouse_require_reserved_records "$FM_ROOT"; then
fm_lock_release "$lock"
return 1
fi
home=$(cd "$FM_ROOT" && treehouse get --lease --lease-holder "$id") || {
fm_lock_release "$lock"
echo "error: treehouse get --lease failed to lease a firstmate home" >&2
return 1
}
fm_lock_release "$lock"
[ -n "$home" ] || { echo "error: treehouse get --lease did not report a firstmate home" >&2; return 1; }
printf '%s\n' "$home"
}
Expand Down
19 changes: 10 additions & 9 deletions bin/fm-install-treehouse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
# 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, the version exercised by the local real-Herdr suite;
# Firstmate's slot ownership needs the lease identities of v2.1.0 or newer.
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). Official 2.3.0 archives are under 8 MiB.
FM_TREEHOUSE_CI_MAX_BYTES=15000000
FM_TREEHOUSE_CI_REPO=kunchenguid/treehouse

Expand All @@ -30,19 +31,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 +69,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 +83,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
90 changes: 58 additions & 32 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,11 @@
# root Firstmate home's state directory before slot allocation and holds it through
# task metadata publication. Teardown holds that same lock while proving and
# returning a slot, so allocation cannot reuse a slot before its owner record
# is published. Under that same lock it writes the slot's owner claim, which is
# what lets teardown leave a slot reassigned since untouched; bin/fm-wake-lib.sh
# owns the claim and bin/fm-teardown.sh owns what it protects. A slot that
# cannot be claimed refuses the spawn rather than launching a worker whose slot
# could later be released out from under its successor. A spawn that aborts
# while it still holds the allocation lock drops its own claim; an abort after
# metadata publication has released that lock leaves the claim in place, and
# the next spawn's claim replaces it.
# is published. Slot leasing, legacy-record preflight, and owner binding are
# owned by bin/fm-wake-lib.sh. A spawn that aborts while it still holds that
# lock, with no surviving record, has launched no worker, so it returns its own
# lease and drops its claim; an abort after the lock is released leaves both
# reserved until they are reconciled by hand.
# The local root is whatever bin/fm-wake-lib.sh's
# fm_firstmate_root_home resolves, so a home seeded from another machine anchors
# that lock itself rather than failing to resolve one;
Expand Down Expand Up @@ -1083,6 +1080,7 @@ SPAWN_TASK_SET_LOCK_HELD=0
SPAWN_TREEHOUSE_PROJECT_LOCK=
SPAWN_TREEHOUSE_PROJECT_LOCK_HELD=0
SPAWN_SLOT_CLAIMED=0
SPAWN_LEASE_HOLDER=
RELAUNCH_REPLACEMENT_PENDING=0
RELAUNCH_REPLACEMENT_BUSY_GEN=
RELAUNCH_REPLACEMENT_HARNESS=
Expand Down Expand Up @@ -1118,6 +1116,27 @@ parse_orca_worktree_result() {
fi
}

spawn_return_aborted_lease() {
local pool slots slot rc=0
if ! fm_treehouse_require_jq \
|| ! pool=$(cd "$PROJ_ABS" && treehouse status --json 2>/dev/null) \
|| ! slots=$(printf '%s\n' "$pool" | jq -r --arg holder "$SPAWN_LEASE_HOLDER" \
'.[] | select(.lease_holder == $holder) | .path'); then
echo "warning: could not read Treehouse's leases for $PROJ_ABS; any lease task $ID's aborted spawn took stays reserved under holder $SPAWN_LEASE_HOLDER until it is returned by hand" >&2
return 1
fi
while IFS= read -r slot; do
[ -n "$slot" ] || continue
if ! (cd "$PROJ_ABS" && treehouse return --force --if-lease-holder "$SPAWN_LEASE_HOLDER" "$slot") >/dev/null 2>&1; then
echo "warning: could not return task $ID's aborted Treehouse lease on $slot; it stays reserved under holder $SPAWN_LEASE_HOLDER until it is returned by hand" >&2
rc=1
fi
done <<EOF
$slots
EOF
return "$rc"
}

spawn_abort_cleanup() {
local status=$?
if [ "$RELAUNCH_REPLACEMENT_PENDING" = 1 ] &&
Expand Down Expand Up @@ -1214,21 +1233,30 @@ spawn_abort_cleanup() {
SPAWN_META_LOCK_HELD=0
fm_lock_release "$SPAWN_META_LOCK" || true
fi
# A spawn that aborts after claiming its slot but before its record survives
# must not leave a claim naming a task no record describes. The release is a
# read-then-remove, so it runs only while the project lock that wrote the
# claim is still held (aborts before metadata publication); a later abort has
# already released that lock and leaves the claim for the next spawn's
# atomic replacement rather than racing it. The release itself never removes
# another task's claim.
if [ "$SPAWN_SLOT_CLAIMED" = 1 ] && [ -n "${WT:-}" ] &&
[ ! -e "$STATE/$ID.meta" ] && [ ! -L "$STATE/$ID.meta" ] &&
fm_treehouse_pool_slot "$PROJ_ABS" "$WT"; then
# A spawn that aborts after taking its lease but before its record survives
# must leave neither a lease nor a claim naming a task no record describes.
# While the project lock that allocated the slot is still held no worker has
# been launched, so every lease Treehouse itself records under this task's
# holder is returned - found from Treehouse's lease record, never the pane's
# possibly stale path, and conditioned on that holder so a slot Treehouse has
# handed elsewhere is never touched - and the claim, a read-then-remove, is
# dropped. A get still running in the pane when the wait times out can take
# its lease after this runs, and a later abort has already released the lock;
# those leases and claims stay reserved, and nothing reissues such a slot
# until it is reconciled by hand. Neither step ever removes another task's
# lease or claim.
if [ -n "$SPAWN_LEASE_HOLDER" ] && [ "$SPAWN_TREEHOUSE_PROJECT_LOCK_HELD" = 1 ] \
&& [ ! -e "$STATE/$ID.meta" ] && [ ! -L "$STATE/$ID.meta" ]; then
spawn_return_aborted_lease || true
fi
if [ "$SPAWN_SLOT_CLAIMED" = 1 ] && [ -n "${WT:-}" ] \
&& [ ! -e "$STATE/$ID.meta" ] && [ ! -L "$STATE/$ID.meta" ] \
&& fm_treehouse_pool_slot "$PROJ_ABS" "$WT"; then
SPAWN_SLOT_CLAIMED=0
if [ "$SPAWN_TREEHOUSE_PROJECT_LOCK_HELD" = 1 ]; then
fm_treehouse_slot_owner_release "$WT" "$ID" || true
else
echo "warning: leaving task $ID's slot claim on $WT in place; the Treehouse project lock is no longer held, so the next spawn's claim replaces it" >&2
echo "warning: leaving task $ID's slot claim and Treehouse lease (holder $SPAWN_LEASE_HOLDER) on $WT in place; the Treehouse project lock is no longer held, so that slot stays reserved and is not reissued until it is reconciled by hand" >&2
fi
fi
if [ "$SPAWN_TREEHOUSE_PROJECT_LOCK_HELD" = 1 ]; then
Expand Down Expand Up @@ -3818,7 +3846,15 @@ 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'
fm_treehouse_require_reserved_records "$PROJ_ABS" || exit 1
SPAWN_LEASE_HOLDER=$(fm_treehouse_lease_holder "$ID" "$FM_HOME") || exit 1
# Keep acquisition in the task shell so Treehouse uses the same pool config
# and environment as before. A durable lease survives this shell's exit.
# The worker then runs in a child shell inside the slot, as the interactive
# `treehouse get` subshell did: the endpoint's own shell stays in the project,
# so teardown's reap of processes under the slot never ends the endpoint
# itself before its own locked close.
spawn_send_text_line "$WT_TARGET" "fm_slot=\$(treehouse get --lease --lease-holder $(shell_quote "$SPAWN_LEASE_HOLDER")) && [ -n \"\$fm_slot\" ] && ( cd -- \"\$fm_slot\" && exec \"\${SHELL:-/bin/sh}\" )"

# 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 @@ -3879,18 +3915,8 @@ elif [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then

validate_spawn_worktree "treehouse get" "$T"

# Claim the pool slot for this task. The interactive `treehouse get` sent to
# the pane above records only a process lease (Treehouse's durable
# `get --lease --lease-holder`, which bin/fm-home-seed.sh uses for secondmate
# homes, is not this path), so Treehouse cannot say which task a slot belongs
# to once that task's worker exits - and that is exactly when the slot is
# handed on and this task's worktree= line goes stale. The claim is what lets
# bin/fm-teardown.sh leave a slot that has since been reassigned untouched, so
# a slot that cannot be claimed is refused here, at the cheapest point, rather
# than launching a worker whose slot teardown could later release out from
# 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.
# Bind the native lease to this task before refreshing or launching. The
# shared project lock covers allocation, claim, and metadata publication.
if fm_treehouse_pool_slot "$PROJ_ABS" "$WT"; then
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
Expand Down
Loading
Loading