Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ Hold-for-return is the default and the only reach profile this release records:
- The away session acts on the captain's words.
It reads them at the tail of every wake, decides by its own judgment whether the event in front of it is the moment they name, acts on them only through the guarded scripts under standing authority, never by analogy, holds with verdict captain on doubt, and opens every outcome summary for an action taken under the words with "per your away instructions:" (`bin/fm-branch-prompt.sh` "Postures" owns the execution rules).
Destructive, irreversible, and security-sensitive actions are never pre-authorizable whatever the words say, and ask-user findings keep the `ask-user-authority` policy unless the words pre-answer the exact decision; anything else that needs the captain holds for their return.
- On Pi, main is parked and the supervision branch handles every safe actionable wake under main's standing authority, through the same guarded scripts main would use: any pull request green at its live head may merge (which one the words meant is the branch's reading), queued work whose blockers cleared - already queued, or filed by the branch because the words explicitly call for it - dispatches within the spend cap, and a decision is answered with the captain's own pre-stated answer or under `ask-user-authority`.
Anything else holds for the return, a red merge never proceeds while away, local-only landing always waits for the captain, and only a wake the branch declines (including a broken branch or unsafe scan) or a watcher failure wakes main (`docs/pi-supervision-branch.md` "Postures").
- On Pi, main is parked and the supervision branch handles every safe actionable wake under main's standing authority through the same guarded scripts main would use; [Authority relocation](../../../docs/pi-supervision-branch.md#authority-relocation) owns the merge, fresh-spawn, relaunch, spend-cap, and decision-answer gates.
Anything requiring the captain holds for the return, and only a wake the branch declines (including a broken branch or unsafe scan) or a watcher failure wakes main (`docs/pi-supervision-branch.md` "Postures").
- On a non-Pi home that runs the supervision host, the host's engine is that branch under the same rules, and a wake it hands back reaches main through that harness's own wake path (`Stop hook feedback` on Claude, a `watcher` follow-up on Cursor, OpenCode, and omp, the arm's background-task-completed notification on Grok, the checkpoint's output on Codex) with a `supervision-host:` line: that is automatic supervision, never the captain's return, so handle it under the away posture ([supervision protocol](../../../docs/supervision-protocols/supervision-host.md)).
- The session-start digest reports the posture under its AFK subsection, so a restart re-enters the posture from the record, not from memory.

Expand Down
2 changes: 1 addition & 1 deletion bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3490,7 +3490,7 @@ if fm_backlog_transition_applies "$CONFIG" "$DATA" "$KIND"; then
exit 1
fi
spawn_preflight_actor=$(fm_lease_actor) || exit "$FM_LEASE_REFUSE_EXIT"
if [ "$spawn_preflight_actor" = branch ] && fm_lease_away_relocated; then
if [ "$RELAUNCH" -ne 1 ] && [ "$spawn_preflight_actor" = branch ] && fm_lease_away_relocated; then
if [ "$BACKLOG_ROW_STATE" != "queued no no" ]; then
echo "error: spawn refused - the supervision branch under the away-posture record may dispatch only queued unblocked work (already queued, or filed by the branch from the captain's away words); task $ID has no dispatchable backlog item in this home" >&2
exit 1
Expand Down
10 changes: 8 additions & 2 deletions docs/pi-supervision-branch.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,14 @@ Each relocated script keeps its own gate, enforcing exactly what a script can ch
| Script | Gate while away |
| --- | --- |
| `bin/fm-pr-merge.sh` | Merges any pull request green at its live head, synchronously, under the record lock, and refuses `--allow-red` and `--allow-missing` while away, so the green gate is absolute in this posture; which pull request the words meant is the branch's reading. |
| `bin/fm-spawn.sh` | Dispatches only queued work whose blockers cleared - already queued, or filed by the branch because the words explicitly call for it; refuses a fresh ordinary spawn for either actor once the home holds as many ordinary task records as the record's spend cap (relaunches and secondmates exempt). |
| `bin/fm-spawn.sh` | A fresh branch spawn dispatches only queued work whose blockers cleared - already queued, or filed by the branch because the words explicitly call for it; refuses a fresh ordinary spawn for either actor once the home holds as many ordinary task records as the record's spend cap (relaunches and secondmates exempt). |
| `bin/fm-send.sh --resolve-key` | Answers a decision the words pre-answer, or one `ask-user-authority`'s judgment (carried verbatim in the branch prompt) lets firstmate decide. |
| `bin/fm-merge-local.sh` | Never relocated. |

An existing task's relaunch through `bin/fm-control.sh` can continue its next stage with an In-flight backlog item, including at the spend cap, because it replaces a worker that already counts.
Where backlog preflight applies, relaunch still requires a readable, dispatchable item with no hold or time gate; the queued-only restriction applies to fresh branch spawns.
The [relaunch transaction](agent-control.md#transactional-relaunch) retains its ordinary lifecycle safety checks.

The merge-authority record and the outcome row's summary are the audit trail.
The return brief renders the words verbatim beside that account.

Expand Down Expand Up @@ -652,7 +656,9 @@ At that moment the branch reports any refusal instead of concluding there is "no
- Prompt stability, including the landed-work cleanup instruction and the second-mate relay, signal-span, and stale-liveness rules.
- Store append-only behavior, the captain cursor barrier, processed-marker sequence bounds and absent-marker safety, and captain-only recorded ages.
- Leases, guards, and non-branch-home invariance.
- The away relocation: only under a valid live record, never for local-only landing, queued-only branch dispatch rather than orphaned in-flight recovery, the spend cap for both actors and its lock-held recheck, and the attended guarded-action behavior restored by archive or an invalid record.
- The away relocation: only under a valid live record, never for local-only landing, queued-only fresh branch dispatch, the spend cap for both actors and its lock-held recheck, and the attended guarded-action behavior restored by archive or an invalid record.

`tests/fm-control-relaunch.test.sh` covers the away branch relaunching an existing In-flight task at the spend cap with its worktree, endpoint, and backlog state preserved, a fresh branch spawn still refusing an In-flight item, and a relaunch still refusing a held item.

`tests/fm-afk-return.test.sh` covers the ordered cleanup-due section, its durable merge-marker requirement, and exclusion of both a done task without durable merge evidence and a persistent secondmate carrying that evidence.

Expand Down
74 changes: 74 additions & 0 deletions tests/fm-control-relaunch.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,80 @@ test_relaunch_moves_a_drifted_item_back_in_flight() {
pass "relaunch heals an item that drifted out of In flight while the task stayed live"
}

test_away_branch_relaunch_accepts_an_in_flight_item() {
local dir out rc=0
fm_tasks_axi_compatible || {
pass "skipped: away relaunch backlog test needs compatible tasks-axi"
return 0
}
dir=$(new_case away-inflight rl-away1)
add_ship_task "$dir" rl-away1 claude
seed_backlog "$dir" rl-away1 in_flight
FM_HOME="$dir/home" "$ROOT/bin/fm-afk-contract.sh" enter --spend 1 >/dev/null \
|| fail "could not enter the fixture's away posture"
break_tasks_axi_start "$dir"

out=$(FM_SUPERVISION_ACTOR=branch run_control "$dir" rl-away1 relaunch \
--note "move the existing task to its next stage") || rc=$?
expect_code 0 "$rc" "the away branch must relaunch its existing In-flight task even at the spend cap"$'\n'"$out"
[ "$(journal_field "$dir" rl-away1 phase)" = complete ] \
|| fail "the away relaunch transaction did not complete"
[ "$(meta_field "$dir" rl-away1 worktree)" = "$dir/wt" ] \
|| fail "the away relaunch replaced the task's worktree"
[ "$(meta_field "$dir" rl-away1 window)" = "fmses:fm-rl-away1" ] \
|| fail "the away relaunch replaced the task's endpoint"
[ "$(backlog_state "$dir" rl-away1)" = in_flight ] \
|| fail "the away relaunch changed the In-flight backlog state"
pass "the away branch relaunches an existing In-flight task through fm-control at the spend cap"
}

test_away_branch_fresh_spawn_still_refuses_an_in_flight_item() {
local dir out rc=0
fm_tasks_axi_compatible || {
pass "skipped: away fresh-spawn backlog test needs compatible tasks-axi"
return 0
}
dir=$(new_case away-fresh rl-away2)
seed_backlog "$dir" rl-away2 in_flight
FM_HOME="$dir/home" "$ROOT/bin/fm-afk-contract.sh" enter --spend 1 >/dev/null \
|| fail "could not enter the fixture's away posture"

out=$(FM_SUPERVISION_ACTOR=branch run_spawn "$dir" rl-away2 \
--mode no-mistakes --yolo off) || rc=$?
expect_code 1 "$rc" "a fresh away branch spawn must still require queued work"$'\n'"$out"
assert_contains "$out" "queued unblocked work" "fresh away spawn lost its queued-work guard"
assert_absent "$dir/home/state/rl-away2.meta" "a refused fresh spawn published a task record"
[ "$(backlog_state "$dir" rl-away2)" = in_flight ] \
|| fail "the refused fresh spawn changed the backlog state"
pass "a fresh away branch spawn still refuses an In-flight backlog item"
}

test_away_branch_relaunch_still_refuses_a_held_item() {
local dir out rc=0
fm_tasks_axi_compatible || {
pass "skipped: held away relaunch backlog test needs compatible tasks-axi"
return 0
}
dir=$(new_case away-held rl-away3)
add_ship_task "$dir" rl-away3 claude
seed_backlog "$dir" rl-away3 in_flight
tasks-axi hold rl-away3 --reason "decision pending" --kind captain \
--file "$dir/home/data/backlog.md" >/dev/null || fail "could not hold the fixture item"
FM_HOME="$dir/home" "$ROOT/bin/fm-afk-contract.sh" enter --spend 1 >/dev/null \
|| fail "could not enter the fixture's away posture"

out=$(FM_SUPERVISION_ACTOR=branch run_control "$dir" rl-away3 relaunch \
--note "retry the held stage") || rc=$?
expect_code 1 "$rc" "the away relaunch exemption must preserve the held-row refusal"$'\n'"$out"
assert_contains "$out" "state in_flight yes no" "the held-row refusal lost its ineligible state"
assert_no_grep "Firstmate operational input waiting: read" "$dir/fake/literal" \
"a held task launched a replacement worker"
pass "the away branch relaunch still refuses a held In-flight backlog item"
}

test_away_branch_relaunch_accepts_an_in_flight_item
test_away_branch_fresh_spawn_still_refuses_an_in_flight_item
test_away_branch_relaunch_still_refuses_a_held_item
test_same_harness_relaunch_keeps_identity_and_reuses_the_endpoint
test_relaunch_refuses_before_exit_when_the_composer_holds_pending_text
test_relaunch_refuses_before_exit_when_the_composer_state_is_unproven
Expand Down
Loading