Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6746206
Automate terminal crew reaping
ruby-dlee Jul 26, 2026
acb3798
Fix native Herdr terminal teardown
ruby-dlee Jul 26, 2026
d2c1adf
no-mistakes(review): Captain, fail closed on uncertain Treehouse leas…
ruby-dlee Jul 26, 2026
887907e
no-mistakes(review): Captain, reject malformed Treehouse lease absenc…
ruby-dlee Jul 26, 2026
5cf72d7
no-mistakes(test): Stabilize watcher handoff and lock takeover tests
ruby-dlee Jul 27, 2026
1cb548a
no-mistakes(test): Isolate account recovery fixtures from concurrent …
ruby-dlee Jul 27, 2026
c064c72
Fix generation-scoped task temp ownership
ruby-dlee Jul 27, 2026
6270823
no-mistakes(document): Correct auto-reap architecture documentation
ruby-dlee Jul 27, 2026
3f0e3c5
no-mistakes(lint): Fix ShellCheck ownership and test-script warnings,…
ruby-dlee Jul 27, 2026
aba391f
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
9e1085f
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
e42b13c
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
da037af
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
f83e569
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
3f78bb1
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
6a2fb8e
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
dfb8b8e
no-mistakes: apply CI fixes
ruby-dlee Jul 27, 2026
d4baf19
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
7e4d33c
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
869ea07
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
8a76e60
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
38fe4cb
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
d41b3c9
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
b161a6b
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
cab957b
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
b314054
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
c747b8e
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
048911c
no-mistakes: apply CI fixes
ruby-dlee Jul 28, 2026
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
15 changes: 13 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,12 @@ A ship task's path from `done` to landed on `main` is set by the project's `mode

- **no-mistakes** - the stages below as written: no-mistakes validation pipeline -> PR -> captain merge.
- **direct-PR** - no pipeline. The crewmate pushes and opens the PR itself (its brief says so) and reports `done: PR <url>`. Skip the Validate step and go straight to PR ready (run `fm-pr-check`, relay the PR). Teardown uses the normal landed-work check.
- **local-only** - no remote, no PR. The crewmate stops at `done: ready in branch fm/<id>`. Review the diff with `bin/fm-review-diff.sh <id>`, relay a one-paragraph summary to the captain, and on approval run `bin/fm-merge-local.sh <id>` to fast-forward local `main` (it refuses anything but a clean fast-forward - if it does, have the crewmate rebase). No `fm-pr-check`. Then teardown, whose safety check requires the branch already merged into local `main`, OR the work pushed to any remote (a fork counts - relevant for upstream-contribution PRs on a local-only-registered project).
- **local-only** - no remote, no PR.
The crewmate stops at `done: ready in branch fm/<id>`.
Review the diff with `bin/fm-review-diff.sh <id>`, relay a one-paragraph summary to the captain, and on approval run `bin/fm-merge-local.sh <id>` to fast-forward local `main`; it refuses anything but a clean fast-forward, and on success immediately invokes fail-closed auto-reaping.
If either the merge or automatic teardown refuses, inspect the retained state rather than weakening the safety check.
No `fm-pr-check`.
Teardown requires the branch already merged into local `main`, OR the work pushed to any remote; a fork counts, which is relevant for upstream-contribution PRs on a local-only-registered project.

When reviewing any crewmate branch diff, use `bin/fm-review-diff.sh <id>` rather than `git diff <default>...branch` directly.
Pooled clones keep their local default refs frozen at clone time and can lag `origin`; the helper always compares against the authoritative base.
Expand Down Expand Up @@ -540,6 +545,11 @@ The helper defaults to `--squash`, accepts explicit merge-method flags such as `
bin/fm-teardown.sh <id>
```

The watcher normally invokes `bin/fm-auto-reap.sh` as soon as a terminal `done` task's recorded PR is provably merged.
The approved local-only merge helper and completed-scout signal take the same automatic path.
Auto-reaping cancels only an exactly attributed no-mistakes run, never guesses cross-branch process ownership, and then calls this ordinary teardown without `--force`.
Persistent secondmates are excluded, and X-mode-linked tasks wait for their required final follow-up.
An automatic refusal is an actionable wake and retains its metadata, worktree, and acquisition authority; after resolving the reported cause, retry with the ordinary command above.
The script refuses if the worktree holds uncommitted changes or committed work that has not landed; treat a refusal as a stop-and-investigate, not an obstacle.
Teardown validates that the recorded project and worktree are exact roots with the expected repository registration, quiesces every ordinary task endpoint, and then runs the final non-destructive safety checks before any Treehouse return.
For a task whose metadata carries `report_required=1`, teardown also publishes the validated completion report before releasing the account lease or removing the worktree.
Expand All @@ -566,7 +576,8 @@ A scout task follows Intake, Spawn, and Supervise exactly as above - scaffold th

- There is no Validate or PR-ready stage. When the crewmate's status says `done`, read `data/<id>/report.md`.
- Relay the findings to the captain: plain chat for a focused answer, lavish-axi when the report has structure worth a visual (multiple findings, options, a plan).
- Tear down immediately - no merge gate. For a post-cutover scout, `bin/fm-teardown.sh` requires the report's completion sections and publishes it before removing the declared scratch worktree; a missing or incomplete report refuses teardown because the findings are the work product.
- The watcher automatically tears down on the terminal `done` signal - no merge gate.
For a post-cutover scout, ordinary teardown still requires the report's completion sections and publishes it before removing the declared scratch worktree; a missing or incomplete report refuses auto-reaping because the findings are the work product.
- Record it in Done with the report path instead of a PR link using `tasks-axi done` when the default tasks-axi backend is active and compatible, otherwise hand-edit `data/backlog.md` and keep Done to the 10 most recent, then re-evaluate the queue and dispatch only queued work whose blockers are gone and whose time/date gate, if any, has arrived.

When the captain invokes `/reports` or asks to browse, open, search, or summarize completed work, load the `reports` skill.
Expand Down
18 changes: 7 additions & 11 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2653,22 +2653,18 @@ fm_backend_herdr_expected_label_matches() { # <target> [expected-label]
# launch-grade fm_backend_herdr_server_ensure used to spawn new crewmates.
#
# Reading a pane or sending keys to it does not care how the server was
# launched - the pane already exists and the server is up. It only needs the
# server to be running and adapter-owned (this HOME's own certificate names the
# live pid), which fm_backend_herdr_server_adapter_owned proves without the
# closed-shell launch certification. This is what keeps peek/steer working when
# FirstMate itself runs INSIDE the herdr session it manages (HERDR_ENV=1): that
# server was not launched through the crewmate adapter's own closed-shell path, so
# closed_shell_environment_ready is false and the full ensure would try to
# restart+recertify - impossible while the session is occupied by live crewmates and
# FirstMate itself. The SPAWN path deliberately keeps the strict ensure so a new
# crewmate still launches only in a certified closed-shell server.
# launched - the pane already exists and the server is up. Native agent routing
# removed production's server-certificate requirement, so exact pane identity is
# the authority boundary there. The explicit legacy certificate-lifecycle test
# mode retains its adapter-owned-server proof.
fm_backend_herdr_server_reachable_for_readsteer() { # <session>
local session=$1 running
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null \
| fm_backend_herdr_control_jq -r '.server.running // false' 2>/dev/null)
[ "$running" = true ] || return 1
fm_backend_herdr_server_adapter_owned "$session"
if fm_backend_herdr_server_certificate_required; then
fm_backend_herdr_server_adapter_owned "$session"
fi
}

fm_backend_herdr_target_ready() { # <target> [expected-label]
Expand Down
2 changes: 1 addition & 1 deletion bin/backends/orca.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ fm_backend_orca_worktree_create() { # <project-path> <name>
wt_path=$(printf '%s' "$out" | fm_backend_orca_json_get worktree-path 2>/dev/null || true)
proof=unproven
[ -z "$terminal" ] || proof=recorded
printf '%s\t%s\t%s\t%s\t%s' "$wt_id" "$wt_path" "$terminal" "$proof" "$repo_id"
printf '%s\t%s\t%s\t%s\t%s\t%s' "$wt_id" "$wt_path" "$terminal" "$proof" "$repo_id" "$name"
if [ "$status" -ne 0 ] || [ -z "$wt_id" ] || [ -z "$wt_path" ]; then
echo "error: orca worktree create returned incomplete or unsuccessful authority for $name" >&2
return 2
Expand Down
86 changes: 84 additions & 2 deletions bin/fm-account-routing-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,84 @@ fm_account_task_key() { # <home> <task> <attempt>
printf 'fm-%.16s-%s-%s\n' "$home_hash" "$task" "$attempt"
}

fm_tasktmp_path() { # <task> <generation>
local task=$1 generation=$2 token
fm_account_valid_id "$task" || return 1
token=${generation##*:}
fm_account_valid_id "$token" || return 1
printf '/tmp/fm-%s-%s\n' "$task" "$token"
}

fm_tasktmp_owner_record() { # <state> <task> <generation>
local state=$1 task=$2 generation=$3 token
fm_account_valid_id "$task" || return 1
token=${generation##*:}
fm_account_valid_id "$token" || return 1
printf '%s/%s.tasktmp-owner.%s\n' "$state" "$task" "$token"
}

fm_tasktmp_owner_write() { # <state> <home> <task> <generation> <root>
local state=$1 home=$2 task=$3 generation=$4 root=$5 home_real expected record tmp
home_real=$(cd "$home" 2>/dev/null && pwd -P) || return 1
expected=$(fm_tasktmp_path "$task" "$generation") || return 1
[ "$root" = "$expected" ] || return 1
record=$(fm_tasktmp_owner_record "$state" "$task" "$generation") || return 1
[ ! -e "$record" ] && [ ! -L "$record" ] || return 1
tmp=$(mktemp "$state/.$task.tasktmp-owner.XXXXXX") || return 1
{
printf 'home=%s\n' "$home_real"
printf 'task=%s\n' "$task"
printf 'generation=%s\n' "$generation"
printf 'root=%s\n' "$root"
} > "$tmp" || { rm -f "$tmp"; return 1; }
chmod 600 "$tmp" || { rm -f "$tmp"; return 1; }
if ! ln "$tmp" "$record" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
rm -f "$tmp"
}

fm_tasktmp_owner_validate() { # <record> <home> <task> <generation> <root>
local record=$1 home=$2 task=$3 generation=$4 root=$5 home_real expected
[ -f "$record" ] && [ ! -L "$record" ] || return 1
[ "$(wc -l < "$record" | tr -d ' ')" = 4 ] || return 1
home_real=$(cd "$home" 2>/dev/null && pwd -P) || return 1
expected=$(fm_tasktmp_path "$task" "$generation") || return 1
[ "$root" = "$expected" ] || return 1
[ "$(sed -n '1s/^home=//p' "$record")" = "$home_real" ] || return 1
[ "$(sed -n '2s/^task=//p' "$record")" = "$task" ] || return 1
[ "$(sed -n '3s/^generation=//p' "$record")" = "$generation" ] || return 1
[ "$(sed -n '4s/^root=//p' "$record")" = "$root" ] || return 1
}

fm_tasktmp_create() { # <state> <home> <task> <generation> <root>
local state=$1 home=$2 task=$3 generation=$4 root=$5 proof="$5/.fm-tasktmp-owner" record
record=$(fm_tasktmp_owner_record "$state" "$task" "$generation") || return 1
fm_tasktmp_owner_write "$state" "$home" "$task" "$generation" "$root" || return 1
if ! mkdir "$root"; then
rm -f "$record"
return 1
fi
if ! cp "$record" "$proof" || ! chmod 600 "$proof" || ! mkdir "$root/gotmp"; then
rm -rf "$root"
rm -f "$record"
return 1
fi
}

fm_tasktmp_remove_owned() { # <state> <home> <task> <generation> <root>
local state=$1 home=$2 task=$3 generation=$4 root=$5 record
record=$(fm_tasktmp_owner_record "$state" "$task" "$generation") || return 1
fm_tasktmp_owner_validate "$record" "$home" "$task" "$generation" "$root" || return 1
if [ -e "$root" ] || [ -L "$root" ]; then
[ -d "$root" ] && [ ! -L "$root" ] || return 1
fm_tasktmp_owner_validate "$root/.fm-tasktmp-owner" "$home" "$task" "$generation" "$root" || return 1
rm -rf "$root" || return 1
fi
rm -f "$record"
}

fm_account_ps_bin() {
if fm_account_test_lab_enabled \
&& [ "${FM_ACCOUNT_TEST_HOOKS:-}" = firstmate-account-tests-v1 ] \
Expand Down Expand Up @@ -1195,7 +1273,7 @@ fm_account_meta_value() { # <meta> <key>
}

fm_account_restore_artifacts() {
local state=$1 task=$2 backup_name=$3 tasktmp=${4:-} retain=${5:-0} backup name source
local state=$1 task=$2 backup_name=$3 tasktmp=${4:-} retain=${5:-0} backup name source generation owner_record
local PATH=$FM_ACCOUNT_SYSTEM_PATH
[ -n "$backup_name" ] || return 0
case "$backup_name" in
Expand All @@ -1213,11 +1291,15 @@ fm_account_restore_artifacts() {
fi
done
if [ -n "$tasktmp" ]; then
[ "$tasktmp" = "/tmp/fm-$task" ] || return 1
generation=$(sed -n '3s/^generation=//p' "$tasktmp/.fm-tasktmp-owner" 2>/dev/null)
owner_record=$(fm_tasktmp_owner_record "$state" "$task" "$generation") || return 1
fm_tasktmp_owner_validate "$owner_record" "${FM_HOME:?}" "$task" "$generation" "$tasktmp" || return 1
fm_tasktmp_owner_validate "$tasktmp/.fm-tasktmp-owner" "${FM_HOME:?}" "$task" "$generation" "$tasktmp" || return 1
if [ -e "$backup/tasktmp-existed" ]; then
[ -e "$backup/gotmp-existed" ] || fm_account_system_exec "$FM_ACCOUNT_SYSTEM_RM_BIN" -rf "$tasktmp/gotmp" || return 1
else
fm_account_system_exec "$FM_ACCOUNT_SYSTEM_RM_BIN" -rf "$tasktmp" || return 1
fm_account_system_exec "$FM_ACCOUNT_SYSTEM_RM_BIN" -f "$owner_record" || return 1
fi
fi
[ "$retain" = 1 ] || fm_account_system_exec "$FM_ACCOUNT_SYSTEM_RM_BIN" -rf "$backup"
Expand Down
Loading
Loading