Skip to content
Closed
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 lease-lifecycle floor or a build missing one of the required lifecycle capabilities; treat it as an upgrade request and rely on `bin/fm-bootstrap.sh` for the exact compatibility check.
For `no-mistakes`, this also covers an installed version older than 1.31.2, because crewmate validation briefs delegate gate mechanics to no-mistakes' version-matched guidance.
For `tasks-axi`, this also covers an installed build that fails the compatibility probe (`docs/configuration.md` "Backlog backend" owns the definition); `config/backlog-backend=manual` only suppresses the verbose `BOOTSTRAP_INFO: tasks-axi available` fact, not this missing-tool report.
For `quota-axi`, bootstrap requires it because firstmate reads its current output directly before resolving every crew-dispatch profile array; without it, report the missing requirement and do not choose around an unexamined candidate.
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,13 @@ jobs:
command -v herdr >/dev/null || { echo "::error::herdr not on PATH after install"; exit 1; }
command -v treehouse >/dev/null || { echo "::error::treehouse not on PATH after install"; exit 1; }
herdr --version
treehouse --version
[ "$(treehouse --version | tr -d '[:space:]')" = "v2.1.0" ] || {
echo "::error::expected exact Treehouse pin v2.1.0"
exit 1
}
treehouse get --help | grep -F -- '--lease-holder'
treehouse return --help | grep -F -- '--if-lease-holder'
treehouse status --help | grep -F -- '--json'
status=$(herdr status --json)
printf '%s\n' "$status"
version=$(printf '%s' "$status" | jq -r '.client.version // empty')
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ state/ volatile runtime signals; gitignored
<id>.turn-ended touched by turn-end hooks
<id>.grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown
<id>.kimi-turnend-token firstmate-owned Kimi hook registry token for the task; removed by teardown
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; leased ship/scout worktrees also record treehouse_lease_holder= and treehouse_lease_state=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<id>.herdr-presentation quarantinable attempt and restart-binding journal for Herdr's optional visual projection; never task or endpoint authority; see docs/herdr-backend.md "Optional presentation spaces"
<id>.check.sh authenticated slow poll; the watcher dispatches validated PR data and the byte-identified X shim through trusted repository scripts, runs registered custom checks from hash-validated private snapshots, and rejects every other state check without execution
<id>.check-trust private content binding created by fm-check-register.sh for an intentional custom check
Expand Down
31 changes: 15 additions & 16 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@
# (herdr-shape): `workspace list`'s `current_directory` field reflects a
# `cd` run directly in the surface's own top-level shell, but stays
# frozen at wherever that shell was when it launched a foreground
# subshell (exactly what `treehouse get` does) - verified live: a nested
# `bash -c 'cd /Users && exec bash'` left `current_directory` reporting
# the PARENT shell's last cwd, never following into the subshell. Fixed
# with zellij's own pwd-marker-probe workaround, reused verbatim in
# spirit (fm_backend_cmux_current_path below).
# subshell - verified live: a nested `bash -c 'cd /Users && exec bash'`
# left `current_directory` reporting the PARENT shell's last cwd, never
# following into the subshell. The current leased-worktree path uses a
# top-level `cd`; the zellij-shaped pwd-marker probe remains the
# authoritative entered-path check (fm_backend_cmux_current_path below).
# 3. `read-screen --lines N` has NO herdr-style small-N empty-result bug -
# verified N=1..10 all return correctly-clamped, non-empty content. The
# "fetch generous, trim locally" pattern is still used for consistency
Expand Down Expand Up @@ -436,15 +436,14 @@ fm_backend_cmux_target_ready() { # <target> [expected-label]
# workaround (bin/backends/zellij.sh:306-347) verbatim in spirit.
#
# Verified pitfall (finding #2 above): cmux's `current_directory` field DOES
# reflect a `cd` run directly in the surface's own top-level shell, but stays
# FROZEN at whatever directory that shell was in when it launched `treehouse
# get` as a foreground command - it never follows that command's own internal
# `cd` into the acquired worktree. cmux's control socket exposes no
# live-process cwd field either (unlike herdr's `foreground_cwd`), so passive
# polling cannot solve this here any more than it could for zellij. Active
# probe instead: print the surface's `$PWD` with a unique marker (atomically
# submitted via send_text_line), briefly settle, then capture and read only
# that marker line. Scoped to fm-spawn.sh's own worktree-discovery poll loop.
# reflect the top-level `cd` that fm-spawn uses to enter a leased worktree, but
# stays FROZEN when that shell launches a nested foreground subshell and never
# follows the child process's internal `cd`. cmux's control socket exposes no
# live-process cwd field either (unlike herdr's `foreground_cwd`), so the active
# probe remains the authoritative entered-path check: print the surface's `$PWD`
# with a unique marker (atomically submitted via send_text_line), briefly settle,
# then capture and read only that marker line. Scoped to fm-spawn.sh's own
# worktree-discovery poll loop.
fm_backend_cmux_current_path() { # <target> [expected-label]
local target=$1 expected_label=${2:-} out line marker_begin="__FM_CMUX_CWD_BEGIN__" marker_end="__FM_CMUX_CWD_END__" in_block=0 chunk="" last=""
fm_backend_cmux_target_ready "$target" "$expected_label" || return 0
Expand Down Expand Up @@ -504,8 +503,8 @@ fm_backend_cmux_send_key() { # <target> <key> [expected-label]
# fm_backend_cmux_send_text_line: send one line of TEXT then submit. cmux has
# no single-call atomic "run and submit" primitive (like herdr's `pane run`),
# so this composes send (literal) + send-key enter, exactly like zellij's
# equivalent - used for the fixed spawn-time commands (treehouse get, the
# GOTMPDIR export).
# equivalent - used for the fixed spawn-time commands (cd into the leased
# worktree, the GOTMPDIR export).
fm_backend_cmux_send_text_line() { # <target> <text> [expected-label]
fm_backend_cmux_send_literal "$1" "$2" "${3:-}" || return 1
fm_backend_cmux_send_key "$1" Enter "${3:-}"
Expand Down
10 changes: 5 additions & 5 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2275,16 +2275,16 @@ fm_backend_herdr_target_ready() { # <target>

# fm_backend_herdr_current_path: the live FOREGROUND process's cwd, or empty on
# any error. Mirrors tmux's pane_current_path poll used for worktree-path
# discovery after `treehouse get`.
# discovery after the spawn-time cd into the leased worktree.
#
# Verified pitfall: `pane get`'s `.result.pane.cwd` is the pane's cwd AT
# CREATION TIME - the top-level shell's cwd - and does NOT update when that
# shell `cd`s or enters a subshell (as `treehouse get` does). Reading it here
# shell `cd`s (as the spawn-time enter into a leased worktree does). Reading it here
# would make fm-spawn.sh's worktree-discovery poll never see the pane "leave"
# the project directory, since `cwd` stays frozen at the original path forever.
# `.result.pane.foreground_cwd` tracks the ACTUALLY RUNNING foreground
# process's cwd instead, which is what changes when `treehouse get` enters its
# worktree subshell - confirmed live against a real treehouse acquisition.
# process's cwd instead, which is what changes when the pane cds into the
# leased worktree - confirmed live against a real treehouse acquisition.
fm_backend_herdr_current_path() { # <target>
fm_backend_herdr_target_ready "$1" || return 0
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane get "$FM_BACKEND_HERDR_PANE" 2>/dev/null \
Expand All @@ -2293,7 +2293,7 @@ fm_backend_herdr_current_path() { # <target>

# fm_backend_herdr_send_text_line: send one line of TEXT then submit,
# ATOMICALLY - mirrors tmux's `send-keys -t T text Enter`. Used for the fixed
# spawn-time commands (treehouse get, the GOTMPDIR export). `pane run` types
# spawn-time commands (cd into the leased worktree, the GOTMPDIR export). `pane run` types
# the command and submits it in one call (verified).
fm_backend_herdr_send_text_line() { # <target> <text>
fm_backend_herdr_target_ready "$1" || return 1
Expand Down
12 changes: 6 additions & 6 deletions bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
# default (tmux, `backend=` absent) path stays byte-identical. Sourced only
# through bin/fm-backend.sh's fm_backend_source, never directly.
#
# Worktree acquisition (running `treehouse get` inside the pane, and polling
# its cwd) is unchanged by this extraction: P1 scopes only the session
# provider, not the worktree provider, so fm-spawn.sh still drives that part
# inline with these same send/current-path primitives.
# Worktree acquisition (durable treehouse lease under the task id, then cd
# into the leased path and poll the pane cwd) is owned by fm-spawn.sh: P1
# scopes only the session provider, not the worktree provider, so spawn still
# drives that part inline with these same send/current-path primitives.
#
# The verified composer/busy-detection and verify-and-retry-submit primitives
# already live in bin/fm-tmux-lib.sh, shared with the away-mode daemon
Expand Down Expand Up @@ -103,8 +103,8 @@ fm_backend_tmux_current_path() { # <target>

# fm_backend_tmux_send_text_line: send one line of TEXT then Enter, with no
# composer verification - used for the fixed spawn-time commands
# (`treehouse get`, the GOTMPDIR export) that already ran this exact sequence
# inline in fm-spawn.sh. Mirrors `tmux send-keys -t "$T" "<text>" Enter`.
# (cd into the leased worktree, the GOTMPDIR export) that already ran this
# exact sequence inline in fm-spawn.sh. Mirrors `tmux send-keys -t "$T" "<text>" Enter`.
fm_backend_tmux_send_text_line() { # <target> <text>
tmux send-keys -t "$1" "$2" Enter
}
Expand Down
36 changes: 17 additions & 19 deletions bin/backends/zellij.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,15 @@
# stdout, exactly as documented.
# 4. `list-panes --json`'s `pane_cwd` reflects a `cd` run DIRECTLY in the
# pane's own top-level shell within one poll (<0.3s) - but does NOT
# reflect a `cd` performed by a NESTED SUBSHELL the pane's shell
# launched as a foreground command (verified: `treehouse get` opens
# exactly such a subshell). `pane_cwd` stays frozen at wherever the
# pane's shell was when it invoked that foreground command - worse than
# herdr's frozen-cwd trap (herdr at least exposes a `foreground_cwd`
# that tracks this; zellij's CLI exposes no live-process cwd field and
# no per-pane pid to read it from `/proc`/`lsof` either). This directly
# contradicts the design report's assumption ("acceptable for tmux and
# zellij") and required a different implementation strategy - see
# fm_backend_zellij_current_path below and docs/zellij-backend.md
# "Worktree-path discovery: pane_cwd does not track a subshell".
# reflect a `cd` performed by a NESTED SUBSHELL the pane's shell launched
# as a foreground command. `pane_cwd` stays frozen at wherever the pane's
# shell was when it invoked that command - worse than herdr's frozen-cwd
# trap (herdr at least exposes a `foreground_cwd` that tracks this;
# zellij's CLI exposes no live-process cwd field and no per-pane pid to
# read it from `/proc`/`lsof` either). The current leased-worktree path is
# entered with a top-level `cd`, while the active marker probe remains the
# authoritative entered-path check; see fm_backend_zellij_current_path
# below and docs/zellij-backend.md.
# 5. `new-tab` DOES steal focus from an attached client with NO flag to
# suppress it (unlike herdr's --no-focus and tmux's new-window -d).
# Mitigated (fm_backend_zellij_create_task): capture the previously
Expand Down Expand Up @@ -383,15 +381,15 @@ fm_backend_zellij_target_ready() { # <target> [expected-label]

# fm_backend_zellij_current_path: the live pane's cwd, or empty on any error.
# Mirrors tmux's pane_current_path poll used for worktree-path discovery after
# `treehouse get`.
# the spawn-time cd into a leased worktree.
#
# Verified pitfall (docs/zellij-backend.md "Worktree-path discovery: pane_cwd
# does not track a subshell"): `list-panes --json`'s `pane_cwd` DOES reflect a
# `cd` run directly in the pane's own top-level shell, but stays FROZEN at
# whatever directory the pane's shell was in when it launched `treehouse get`
# as a foreground command - it never follows that command's own internal `cd`
# into the acquired worktree, even after the subshell is fully interactive and
# a `pwd` typed into it prints the correct live path on screen. Zellij's CLI
# `cd` run directly in the pane's own top-level shell (what fm-spawn uses after
# a durable treehouse lease), but stays FROZEN at whatever directory the pane's
# shell was in when it launched a nested foreground subshell - it never follows
# that command's own internal `cd`, even after the subshell is fully interactive
# and a `pwd` typed into it prints the correct live path on screen. Zellij's CLI
# exposes no per-pane pid and no live-process cwd field to read instead
# (unlike herdr's `foreground_cwd`), so passive JSON polling cannot solve
# this. Active probe instead: print the pane's `$PWD` with a unique marker
Expand Down Expand Up @@ -462,8 +460,8 @@ fm_backend_zellij_send_key() { # <target> <key> [expected-label]

# fm_backend_zellij_send_text_line: send one line of TEXT then submit,
# ATOMICALLY - mirrors tmux's `send-keys -t T text Enter` / herdr's `pane
# run`. Used for the fixed spawn-time commands (treehouse get, the GOTMPDIR
# export). Zellij has no single-call atomic "run and submit" action, so this
# run`. Used for the fixed spawn-time commands (cd into the leased worktree,
# the GOTMPDIR export). Zellij has no single-call atomic "run and submit" action, so this
# composes paste (literal) + send-keys Enter, exactly like send_literal +
# send_key are composed elsewhere - the two-step form is the ONLY form for
# this adapter, unlike tmux/herdr which have a genuinely atomic primitive.
Expand Down
24 changes: 17 additions & 7 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
# 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 below 2.1.0 or when its installed build
# lacks the get, status, and guarded-return lease lifecycle flags.
# no-mistakes is also MISSING when its installed version is older than
# 1.31.2.
# tasks-axi and quota-axi are required bootstrap tools (same class as
Expand Down Expand Up @@ -536,9 +536,17 @@ if ! BACKEND_TOOLS=$(fm_backend_required_tools "$BACKEND"); then
fi
TOOLS="$BACKEND_TOOLS $COMMON_TOOLS"
NO_MISTAKES_MIN=1.31.2

treehouse_supports_lease() {
treehouse get --help 2>&1 | grep -Eq '(^|[^[:alnum:]_-])--lease([^[:alnum:]_-]|$)'
TREEHOUSE_MIN=2.1.0

treehouse_supports_lease_lifecycle() {
local get_help return_help status_help
get_help=$(treehouse get --help 2>&1) || return 1
return_help=$(treehouse return --help 2>&1) || return 1
status_help=$(treehouse status --help 2>&1) || return 1
printf '%s\n' "$get_help" | grep -Eq '(^|[^[:alnum:]_-])--lease([^[:alnum:]_-]|$)' \
&& printf '%s\n' "$get_help" | grep -Eq '(^|[^[:alnum:]_-])--lease-holder([^[:alnum:]_-]|$)' \
&& printf '%s\n' "$return_help" | grep -Eq '(^|[^[:alnum:]_-])--if-lease-holder([^[:alnum:]_-]|$)' \
&& printf '%s\n' "$status_help" | grep -Eq '(^|[^[:alnum:]_-])--json([^[:alnum:]_-]|$)'
}

# Shared semantic-version floor for the tool gates below. A version string that
Expand Down Expand Up @@ -865,11 +873,13 @@ done
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 lease-lifecycle 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 \
&& { ! tool_version_at_least treehouse "$TREEHOUSE_MIN" \
|| ! treehouse_supports_lease_lifecycle; }; 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
Loading
Loading