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/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ An explicit `--account-pool` or `--account-profile` overrides it for that spawn,
This pool is not inherited into the secondmate home because it governs the primary's secondmate launch, while `config/account-routing-mode` is inherited so the secondmate applies the same off/observe/enforce policy to its own crewmates.

This section is the single owner of the secondmate sync and inheritable-config propagation contract; `AGENTS.md` sections 3 and 4 point here.
Before launch, `fm-spawn.sh --secondmate` locally fast-forwards the home to the primary firstmate checkout's current default-branch commit when it is safe; dirty, diverged, or in-flight homes launch unchanged with a warning.
Before launch, `fm-spawn.sh --secondmate` locally fast-forwards the home to the primary firstmate checkout's current default-branch commit when it is safe; dirty, diverged, or otherwise unresolved homes are refused before launch.
The locked session-start bootstrap sweep runs the same guarded fast-forward for every live secondmate home, discovered from `state/<id>.meta` records with `kind=secondmate` (`data/secondmates.md` only backfills `home=` for older records).
That no-fetch path is a purely local fast-forward of tracked files, never an origin fetch, and it never touches the gitignored operational dirs, so a secondmate's backlog, projects, and in-flight work are never disturbed; a linked worktree advances immediately, while a standalone clone that lacks the target receives firstmate updates through `/updatefirstmate`'s origin refresh.
The same launch and the same locked bootstrap sweep also propagate the primary's declared inheritable local config, currently `config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, and `config/account-routing-mode`, into the secondmate home's `config/`.
Expand Down
15 changes: 6 additions & 9 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2654,15 +2654,11 @@ fm_backend_herdr_expected_label_matches() { # <target> [expected-label]
#
# 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.
# server to be running. The legacy certificate lab additionally requires
# adapter ownership, 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 production server is intentionally not certified by the crewmate adapter.
fm_backend_herdr_server_reachable_for_readsteer() { # <session>
local session=$1 running
if fm_backend_herdr_test_hooks_enabled \
Expand All @@ -2672,6 +2668,7 @@ fm_backend_herdr_server_reachable_for_readsteer() { # <session>
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_certificate_required || return 0
fm_backend_herdr_server_adapter_owned "$session"
}

Expand Down
6 changes: 2 additions & 4 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,6 @@ ORCA_TERMINAL=
ORCA_TERMINAL_PROOF=
ORCA_REPO_ID=
ORCA_EXPECTED_TASK=
ORCA_PROVIDER_TASK=
ID=
ACCOUNT_LEASE_CREATED=0
FM_ACCOUNT_MUTATION_ACQUIRED=0
Expand Down Expand Up @@ -1044,7 +1043,6 @@ parse_orca_worktree_result() {
rest=${rest#*$'\t'}
ORCA_REPO_ID=$rest
case "$ORCA_REPO_ID" in *$'\t'*) return 1 ;; esac
ORCA_PROVIDER_TASK=
}

persist_orca_cleanup_quarantine() {
Expand Down Expand Up @@ -3417,8 +3415,8 @@ EOF
echo "error: cannot durably record Orca create authority for $ID" >&2
exit 1
}
if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ] || [ "$ORCA_PROVIDER_TASK" != "$ORCA_EXPECTED_TASK" ]; then
echo "error: orca did not return matching worktree id, path, and task authority for $W" >&2
if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ]; then
echo "error: orca did not return worktree id and path authority for $W" >&2
exit 1
fi
validate_spawn_worktree "orca worktree create" "$W"
Expand Down
6 changes: 5 additions & 1 deletion bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ quiesce_child_endpoint() {
scoped_target=$(meta_value "$meta" tmux_session_target)
[ "$backend" != orca ] || scoped_target=$(meta_value "$meta" orca_worktree_id)
if [ "$backend" = orca ]; then
[ -n "$target" ] || {
echo "error: child endpoint identity for $task is missing; refusing destructive cleanup" >&2
return 1
}
quiesce_authoritative_orca_endpoint "$target" "$scoped_target" "fm-$task" || {
echo "error: child Orca endpoint authority or quiescence is unproven for $task" >&2
return 1
Expand Down Expand Up @@ -3435,7 +3439,7 @@ validate_firstmate_home_for_removal() {
require_treehouse_task_lease "$abs_home_path" "$expected_id" || return 1
fi
validate_secondmate_home_landed_state "$abs_home_path" "$expected_source" || return 1
if [ -n "$expected_id" ]; then
if [ -n "$expected_id" ] && [ "$source_authority" -eq 1 ]; then
validate_secondmate_project_clones \
"$abs_home_path" "$expected_registry" "$expected_id" "$expected_source" || return 1
fi
Expand Down
78 changes: 72 additions & 6 deletions tests/fm-account-routing.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2221,13 +2221,17 @@ test_enforced_secondmate_requires_routing_inheritance_and_capable_home() {
sm="$CASE_DIR/secondmate-home"
make_seeded_secondmate_home "$sm" "$id" incapable
sm=$(cd "$sm" && pwd -P)
[ -z "$(git -C "$sm" status --porcelain)" ] \
|| fail "incapable secondmate fixture was not a clean pre-routing revision"
printf 'enforce\n' > "$HOME_DIR/config/account-routing-mode"
out=$(FM_TEST_PANE_PATH="$sm" run_spawn "$id" "$sm" --secondmate)
status=$?
[ "$status" -ne 0 ] || fail "enforced secondmate launched from a pre-Agent-Fleet home"
assert_contains "$out" "$sm" "capability refusal omitted the offending secondmate home"
assert_contains "$out" "lacks Agent Fleet routing support" \
"capability refusal did not stop at the capability gate"
assert_contains "$out" "the home lacks Agent Fleet routing support" \
"capability refusal did not reach the capability gate"
assert_contains "$out" "Fast-forward or otherwise reconcile the home" \
"capability refusal omitted the durable reconciliation action"
assert_not_grep '^new-window ' "$TMUX_LOG" "capability refusal created an endpoint"
pass "enforced secondmates require inherited routing policy and Agent Fleet-capable homes"
}
Expand Down Expand Up @@ -2268,12 +2272,66 @@ test_secondmate_routing_inheritance_is_authoritative_for_every_mode() {
sm="$CASE_DIR/secondmate-home"
make_seeded_secondmate_home "$sm" "$id" incapable
sm=$(cd "$sm" && pwd -P)
[ -z "$(git -C "$sm" status --porcelain)" ] \
|| fail "off-mode incapable secondmate fixture was not a clean pre-routing revision"
out=$(FM_TEST_PANE_PATH="$sm" run_spawn "$id" "$sm" --secondmate)
status=$?
[ "$status" -eq 0 ] || fail "routing-off secondmate refused a clean legacy home"
assert_contains "$out" "lacks Agent Fleet routing support" \
"routing-off legacy-home launch omitted its capability warning"
pass "secondmate launches require authoritative routing policy in every mode"
[ "$status" -eq 0 ] || fail "off-mode secondmate rejected a clean pre-routing home (exit $status): $out"
assert_contains "$out" "$id" "off-mode capability warning omitted the offending secondmate"
assert_contains "$out" "launching because account routing is off" \
"off-mode capability warning did not explain the compatibility launch"
assert_contains "$out" "spawned $id" "off-mode compatibility launch did not complete"
assert_grep 'kind=secondmate' "$HOME_DIR/state/$id.meta" \
"off-mode compatibility launch did not publish secondmate metadata"
assert_not_grep '^account_' "$HOME_DIR/state/$id.meta" \
"off-mode compatibility launch published managed account metadata"
[ ! -s "$AF_LOG" ] || fail "off-mode compatibility launch called Agent Fleet"
pass "secondmate launches require authoritative routing policy while off mode preserves legacy homes"
}

test_enforced_dirty_incapable_secondmate_stops_at_freshness_gate() {
local id rec sm out status
id=account-secondmate-enforce-dirty-incapable-z11i
rec=$(make_case secondmate-enforce-dirty-incapable claude)
read_case "$rec"
sm="$CASE_DIR/secondmate-home"
make_seeded_secondmate_home "$sm" "$id" incapable
sm=$(cd "$sm" && pwd -P)
printf '\n' >> "$sm/AGENTS.md"
printf 'enforce\n' > "$HOME_DIR/config/account-routing-mode"

out=$(FM_TEST_PANE_PATH="$sm" run_spawn "$id" "$sm" --secondmate)
status=$?
[ "$status" -ne 0 ] || fail "enforced dirty incapable secondmate launched"
assert_contains "$out" "dirty working tree" \
"enforced dirty incapable secondmate did not stop at the freshness gate"
assert_not_contains "$out" "lacks Agent Fleet routing support" \
"enforced dirty incapable secondmate reached capability handling"
assert_not_grep '^new-window ' "$TMUX_LOG" \
"enforced dirty incapable secondmate created an endpoint"
pass "enforced dirty incapable secondmates stop at the freshness gate"
}

test_off_dirty_incapable_secondmate_stops_at_freshness_gate() {
local id rec sm out status
id=account-secondmate-off-dirty-incapable-z11j
rec=$(make_case secondmate-off-dirty-incapable claude)
read_case "$rec"
sm="$CASE_DIR/secondmate-home"
make_seeded_secondmate_home "$sm" "$id" incapable
sm=$(cd "$sm" && pwd -P)
printf '\n' >> "$sm/AGENTS.md"

out=$(FM_TEST_PANE_PATH="$sm" run_spawn "$id" "$sm" --secondmate)
status=$?
[ "$status" -ne 0 ] || fail "off-mode dirty incapable secondmate launched"
assert_contains "$out" "dirty working tree" \
"off-mode dirty incapable secondmate did not stop at the freshness gate"
assert_not_contains "$out" "launching because account routing is off" \
"off-mode dirty incapable secondmate reached capability handling"
assert_not_grep '^new-window ' "$TMUX_LOG" \
"off-mode dirty incapable secondmate created an endpoint"
pass "off-mode dirty incapable secondmates stop at the freshness gate"
}

test_managed_shared_namespace_secondmate_uses_primary_endpoint_scope() {
Expand Down Expand Up @@ -6129,6 +6187,12 @@ if [ "${FM_TEST_FOCUSED:-}" = secondmate-direct-scope ]; then
exit 0
fi

if [ "${FM_TEST_FOCUSED:-}" = dirty-secondmate-freshness ]; then
run_isolated_test test_enforced_dirty_incapable_secondmate_stops_at_freshness_gate
run_isolated_test test_off_dirty_incapable_secondmate_stops_at_freshness_gate
exit 0
fi

if [ "${FM_TEST_FOCUSED:-}" = review-round-10 ]; then
run_isolated_test test_managed_recovery_accepts_inherited_lifecycle_lock
run_isolated_test test_native_resume_requires_fresh_sessionstart_evidence
Expand Down Expand Up @@ -6435,6 +6499,8 @@ run_isolated_test test_secondmate_pool_routes_when_mode_is_enforced_and_mode_inh
run_isolated_test test_explicit_secondmate_route_preserves_ambient_primary_enforce
run_isolated_test test_enforced_secondmate_requires_routing_inheritance_and_capable_home
run_isolated_test test_secondmate_routing_inheritance_is_authoritative_for_every_mode
run_isolated_test test_enforced_dirty_incapable_secondmate_stops_at_freshness_gate
run_isolated_test test_off_dirty_incapable_secondmate_stops_at_freshness_gate
run_isolated_test test_managed_shared_namespace_secondmate_uses_primary_endpoint_scope
run_isolated_test test_unused_secondmate_pool_never_blocks_unmanaged_spawn
run_isolated_test test_agent_fleet_task_keys_are_namespaced_by_home_and_attempt
Expand Down
Loading
Loading