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
1 change: 1 addition & 0 deletions .agents/skills/bearings/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ It never tears down a task, merges a PR, dispatches new work, steers a worker, a
Keep the default local-only read unless the captain asks to include PRs.
For registered secondmates, use the snapshot's structured-home classification and provenance.
A parent event or bounded terminal contradiction is fallback evidence, never authority over readable structured home state.
The snapshot invocation enables recovery-grade endpoint and worker liveness so a non-held, terminal-looking in-flight row with both signals verified absent appears under `gates` as a measured-liveness gate; held rows and active-worker rows do not receive that gate.
Structured captain-held decisions come from `decision-hold-lifecycle` and appear under `decisions_open`.
Do not scrape reports, visual-review artifacts, raw status-event tails, or visible conversation history to supplement current state.
A queued item under `gates` only becomes "next work" when its blocker is gone and its time/date gate has arrived.
Expand Down
24 changes: 21 additions & 3 deletions bin/fm-bearings-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
# This wrapper consumes canonical status decisions plus canonically normalized
# backlog roles, unresolved blockers, and captain actionability. It never infers
# decisions from report or visual-review prose or reimplements snapshot semantics.
# Each invocation requests recovery-grade endpoint and worker liveness from the
# canonical snapshot. A non-secondmate in-flight row that looks terminal, is not
# held, and has both endpoint and worker verified absent is projected as a
# measured-liveness gate; held rows and rows with an active worker are excluded.
#
# Main-home inventory validity comes from the canonical snapshot's main_inventory
# object (orphan structured in-flight without meta, unstructured current rows).
Expand Down Expand Up @@ -169,12 +173,12 @@ command -v jq >/dev/null 2>&1 || { echo "fm-bearings-snapshot: jq not found" >&2
NOW=${FM_BEARINGS_NOW:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
if [ "$ALL_LANDED" = 1 ] || [ "$ALL_SECONDMATES" = 1 ]; then
if [ "$ALL_LANDED" = 1 ]; then
SNAP=$(FM_SNAPSHOT_NOW="$NOW" FM_SNAPSHOT_SECONDMATES=0 FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME=0 "$FLEET" --json) || exit $?
SNAP=$(FM_SNAPSHOT_NOW="$NOW" FM_SNAPSHOT_MEASURE_AGENT_STATE=1 FM_SNAPSHOT_SECONDMATES=0 FM_SNAPSHOT_SECONDMATE_LANDED_PER_HOME=0 "$FLEET" --json) || exit $?
else
SNAP=$(FM_SNAPSHOT_NOW="$NOW" FM_SNAPSHOT_SECONDMATES=0 "$FLEET" --json) || exit $?
SNAP=$(FM_SNAPSHOT_NOW="$NOW" FM_SNAPSHOT_MEASURE_AGENT_STATE=1 FM_SNAPSHOT_SECONDMATES=0 "$FLEET" --json) || exit $?
fi
else
SNAP=$(FM_SNAPSHOT_NOW="$NOW" "$FLEET" --json) || exit $?
SNAP=$(FM_SNAPSHOT_NOW="$NOW" FM_SNAPSHOT_MEASURE_AGENT_STATE=1 "$FLEET" --json) || exit $?
fi
HOME_LABEL=$(printf '%s' "$SNAP" | jq -er '.fm_home | strings | split("/") | (.[-2:] | join("/"))') \
|| { echo "fm-bearings-snapshot: invalid canonical snapshot" >&2; exit 1; }
Expand Down Expand Up @@ -301,6 +305,15 @@ MODEL=$(printf '%s' "$SNAP" | jq \
--argjson candidate_prs "$CANDIDATE_PRS" '
def trunc($n): if . == null then null else
(tostring | gsub("\\s+"; " ") | if (length > $n) then (.[:$n] + "…") else . end) end;
def measured_liveness_gate:
.kind != "secondmate"
and .backlog.state == "in_flight"
and .backlog.current_role != "held"
and ((.current_state.state == "done" or .current_state.state == "failed")
or ((.hints.last_event_text // "") | test("^(done|failed):")))
and .liveness.activity == "absent"
and .liveness.endpoint.presence == "verified_absent"
and .liveness.worker.presence == "verified_absent";
def round_robin_landed($n):
. as $groups
| [range(0; (($groups | map(length) | max) // 0)) as $i
Expand Down Expand Up @@ -370,6 +383,7 @@ MODEL=$(printf '%s' "$SNAP" | jq \
| select(.kind != "secondmate")
| select(.backlog.current_role != "program")
| select(.backlog.current_role != "held" or .current_state.state == "working")
| select(measured_liveness_gate | not)
| {id, kind,
state: .current_state.state,
doing: ((.current_state.detail // "") as $d
Expand All @@ -394,6 +408,10 @@ MODEL=$(printf '%s' "$SNAP" | jq \
reason:"main inventory",
owner:"(main)"}]
else [] end)
+ [ .tasks[]
| select(measured_liveness_gate)
| {id,title:((.backlog.title // .id) | trunc(60)),blocked_by:"-",
reason:("measured liveness: " + .liveness.activity + "; endpoint=" + .liveness.endpoint.presence + "; worker=" + .liveness.worker.presence | trunc(120)),owner:"(main)"} ]
+ [ .backlog.records[]
| . as $record
| select(.structured and
Expand Down
47 changes: 40 additions & 7 deletions bin/fm-fleet-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
# against current_state; hints.pending_decision and hints.blocked_event are
# booleans derived from that set.
# endpoint.exists is the cheap backend endpoint-presence read.
# endpoint.agent_alive is populated for secondmates only, where it is useful
# return-channel supervision data; other tasks use "not_checked".
# endpoint.agent_alive is populated for secondmates only by default, where it
# is useful return-channel supervision data; other tasks use "not_checked".
# FM_SNAPSHOT_MEASURE_AGENT_STATE=1 asks the existing recovery-grade backend
# classifier for every local task and adds normalized endpoint/worker liveness.
# scout_reports[]: present data/<id>/report.md pointers.
# main_inventory: {valid,reason,orphan_in_flight[],unstructured_current_count} -
# main-home current-inventory checks shared with secondmate_home_summary_json
Expand Down Expand Up @@ -66,6 +68,14 @@ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}"
BACKLOG="$DATA/backlog.md"
SNAPSHOT_NOW=${FM_SNAPSHOT_NOW:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
FM_SNAPSHOT_MEASURE_AGENT_STATE=${FM_SNAPSHOT_MEASURE_AGENT_STATE:-0}
case "$FM_SNAPSHOT_MEASURE_AGENT_STATE" in
0|1) ;;
*)
echo "fm-fleet-snapshot: FM_SNAPSHOT_MEASURE_AGENT_STATE must be 0 or 1" >&2
exit 2
;;
esac
if [ -n "${FM_SNAPSHOT_NOW_EPOCH:-}" ]; then
SNAPSHOT_EPOCH=$FM_SNAPSHOT_NOW_EPOCH
else
Expand Down Expand Up @@ -403,7 +413,7 @@ backlog_json() { # [<backlog-path>] - defaults to this home's $BACKLOG
task_json_lines() {
local meta id kind harness mode yolo project worktree home projects backend target status_log report_path
local remote_host remote_root remote_state remote_rc remote_home_present
local pr pr_source event_json current_json endpoint_exists agent_alive meta_json status_json report_json worktree_json home_json
local pr pr_source event_json current_json endpoint_exists agent_alive agent_state meta_json status_json report_json worktree_json home_json
local last_event_raw current_state current_source pending_decision blocked_event report_present=0 pr_from_status
local open_decisions_tsv open_decisions_json

Expand Down Expand Up @@ -504,15 +514,27 @@ task_json_lines() {
fi
else
if [ -n "$target" ]; then
if fm_backend_target_exists "$backend" "$target" "fm-$id" >/dev/null 2>&1; then
if [ "$kind" = secondmate ] || [ "$FM_SNAPSHOT_MEASURE_AGENT_STATE" = 1 ]; then
agent_state=$(fm_backend_agent_state "$backend" "$target" 2>/dev/null || printf unreadable)
case "$agent_state" in
alive) endpoint_exists=true; agent_alive=alive ;;
dead) endpoint_exists=true; agent_alive=dead ;;
missing) endpoint_exists=false; agent_alive=dead ;;
*)
if fm_backend_target_exists "$backend" "$target" "fm-$id" >/dev/null 2>&1; then
endpoint_exists=true
else
endpoint_exists=null
fi
agent_alive=unknown
;;
esac
elif fm_backend_target_exists "$backend" "$target" "fm-$id" >/dev/null 2>&1; then
endpoint_exists=true
else
endpoint_exists=false
fi
fi
if [ "$kind" = secondmate ] && [ -n "$target" ]; then
agent_alive=$(fm_backend_agent_alive "$backend" "$target" 2>/dev/null || printf unknown)
fi
fi

[ -f "$report_path" ] && report_present=1 || report_present=0
Expand Down Expand Up @@ -581,6 +603,17 @@ task_json_lines() {
elif $agent_alive == "alive" or $agent_alive == "dead" then $agent_alive
else "unknown" end),
observed_at:$observed_at,freshness:"fresh"},
liveness:{
endpoint:{presence:(if $endpoint_exists == true then "verified_present"
elif $endpoint_exists == false then "verified_absent"
else "unverified" end)},
worker:{presence:(if $agent_alive == "alive" then "verified_present"
elif $agent_alive == "dead" then "verified_absent"
else "unverified" end)},
activity:(if $endpoint_exists == false and $agent_alive == "dead" then "absent"
elif $agent_alive == "alive" then "present"
else "unverified" end)
},
pr:{url:($pr | if . == "" then null else . end),source:$pr_source},
hints:{
pending_decision:$pending_decision,
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Decision-only events such as `resolved` never become current state or leak their
In that status-log fallback, a declared external wait reports the distinct `paused` state with its reason.
The semantic branch reports working only on an exact busy verdict and names the source that produced it; an unknown verdict never becomes working, never permits the status-log fallback, and never becomes a silent idle.
For whole-fleet read-only review, `bin/fm-fleet-snapshot.sh --json` emits schema `fm-fleet-snapshot.v1` from the backlog, task metadata, current crew state, endpoint probes, PR/report pointers, scout reports, bounded current summaries from registered secondmate homes, and secondmate return-channel guidance.
When `FM_SNAPSHOT_MEASURE_AGENT_STATE=1` is set, its local task rows also expose normalized endpoint and worker liveness under `liveness`; Bearings enables this mode and projects a non-held, terminal-looking in-flight row with both endpoint and worker verified absent into Charted Next as a measured-liveness gate, while held rows and rows with an active worker remain in their ordinary sections.
`bin/fm-fleet-view.sh` renders that snapshot as Markdown for humans, while `bin/fm-bearings-snapshot.sh` provides the bounded bearings projection, so both views consume one structured contract instead of reparsing raw fleet files.
The script header owns the exact JSON schema.

Expand Down
2 changes: 1 addition & 1 deletion docs/decision-hold-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The final verification commands and their exact summarized outputs follow.

```text
$ bash tests/fm-decision-hold-lifecycle.test.sh
ok - report-only unresolved decision is reproduced and completion refuses before loss
ok - verified-absent in-flight work gates while held and active workers avoid measured-liveness gates, selector neutralization goes RED, and completion refuses before report loss
ok - non-forced scout teardown always requires durable inventory verification
ok - captain holds are idempotent, distinct, teardown-safe, Bearings-visible, and durably routed before close
ok - completion and verification validate origins before constructing paths
Expand Down
117 changes: 105 additions & 12 deletions tests/fm-decision-hold-lifecycle.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,61 @@ EOF
printf '%s\n' "$home"
}

run_bearings() { # <home>
local home=$1
run_bearings_with() { # <bearings-bin> <home>
local bearings=$1 home=$2
PATH="$home/fakebin:$PATH" FM_HOME="$home" FM_BEARINGS_NOW=2026-07-14T12:00:00Z \
"$BEARINGS" --json
"$bearings" --json
}

run_bearings() { # <home>
run_bearings_with "$BEARINGS" "$1"
}

liveness_property_holds() { # <json> <phantom> <held> <active>
printf '%s' "$1" | jq -e --arg phantom "$2" --arg held "$3" --arg active "$4" '
(.gates | any(
.id == $phantom
and .reason == "measured liveness: absent; endpoint=verified_absent; worker=verified_absent"
))
and (.gates | any(.id == $held and .reason == "captain route choice pending"))
and (.gates | any(
.id == $held and (.reason | startswith("measured liveness:"))
) | not)
and (.gates | any(.id == $active) | not)
' >/dev/null
}

install_liveness_tmux_fixture() { # <home> <active-id>
local home=$1 active_id=$2
cat > "$home/fakebin/tmux" <<'EOF'
#!/usr/bin/env bash
target=
previous=
for argument in "$@"; do
if [ "$previous" = -t ]; then target=$argument; fi
previous=$argument
done
case "${1:-}" in
list-windows)
printf 'fm-%s\n' "${FM_TEST_ACTIVE_ID:?}"
;;
display-message)
[ "$target" = "firstmate:fm-${FM_TEST_ACTIVE_ID:?}" ] || exit 1
case "${*: -1}" in
'#{pane_id}') printf '%%1\n' ;;
'#{pane_current_command}') printf 'codex\n' ;;
'#{pane_tty}') printf '\n' ;;
*) printf 'codex\n' ;;
esac
;;
capture-pane)
[ "$target" = "firstmate:fm-${FM_TEST_ACTIVE_ID:?}" ] || exit 1
printf 'working\n'
;;
*) exit 1 ;;
esac
EOF
chmod +x "$home/fakebin/tmux"
}

run_teardown() { # <home> <id>
Expand All @@ -49,13 +100,18 @@ run_teardown() { # <home> <id>
# no held backlog item or open status exists, and the authoritative Bearings view
# correctly omits it. Completion must now refuse before teardown can erase the source.
test_uninventoried_report_decision_refuses_completion() {
local home id json rc
local home id held_id active_id mutated_bin json mutated_json rc
home=$(make_home omitted-decision)
id=sample-route-review
mkdir -p "$home/data/$id"
held_id=sample-held-review
active_id=sample-active-review
mkdir -p "$home/data/$id" "$home/data/$held_id" "$home/data/$active_id" \
"$home/projects/active-scratch"
cat > "$home/data/backlog.md" <<EOF
## In flight
- [ ] $id - Investigate sample routing (repo: sample) (kind: scout) (since 2026-07-14)
- [ ] $held_id - Hold sample routing (repo: sample) (kind: captain) (since 2026-07-14) (hold: captain route choice pending) (hold-kind: captain)
- [ ] $active_id - Continue sample routing (repo: sample) (kind: scout) (since 2026-07-14)

## Queued

Expand All @@ -68,20 +124,57 @@ EOF
"harness=codex" \
"kind=scout" \
"mode=scout"
fm_write_meta "$home/state/$held_id.meta" \
"window=firstmate:fm-$held_id" \
"worktree=$home/projects/missing-held" \
"project=$home/projects/sample" \
"harness=codex" \
"kind=captain" \
"mode=captain"
fm_write_meta "$home/state/$active_id.meta" \
"window=firstmate:fm-$active_id" \
"worktree=$home/projects/active-scratch" \
"project=$home/projects/sample" \
"harness=codex" \
"kind=scout" \
"mode=scout"
printf 'done: report and visual review complete\n' > "$home/state/$id.status"
printf 'needs-decision [key=route]: captain route choice pending\n' > "$home/state/$held_id.status"
printf 'working: continuing sample routing\n' > "$home/state/$active_id.status"
cat > "$home/data/$id/report.md" <<'EOF'
# Sample route review

The evidence is complete.
The captain still needs to choose route north or route south before follow-up work starts.
EOF

json=$(run_bearings "$home") || fail "Bearings failed for unresolved-decision regression"
printf '%s' "$json" | jq -e '
(.decisions_open | length) == 0
and (.gates | length) == 0
and (.reports | any(.id == "sample-route-review"))
' >/dev/null || fail "the pre-policy omission shape was not reproduced: $json"
install_liveness_tmux_fixture "$home" "$active_id"
json=$(FM_TEST_ACTIVE_ID="$active_id" run_bearings "$home") \
|| fail "Bearings failed for unresolved-decision regression"
liveness_property_holds "$json" "$id" "$held_id" "$active_id" \
|| fail "measured liveness did not distinguish the phantom, held, and active lanes without hiding the held gate: $json"
printf '%s' "$json" | jq -e --arg id "$id" '
(.decisions_open | any(.id == $id) | not) and (.reports | any(.id == $id))
' >/dev/null || fail "phantom regression lost its report-only decision shape: $json"

mutated_bin="$home/mutated-bin"
cp -R "$ROOT/bin" "$mutated_bin"
sed 's/and \.liveness\.activity == "absent"/and false/' \
"$BEARINGS" > "$mutated_bin/fm-bearings-snapshot.sh"
chmod +x "$mutated_bin/fm-bearings-snapshot.sh"
cmp -s "$BEARINGS" "$mutated_bin/fm-bearings-snapshot.sh" \
&& fail "measured-liveness selector mutation did not alter the executable"
mutated_json=$(FM_TEST_ACTIVE_ID="$active_id" \
run_bearings_with "$mutated_bin/fm-bearings-snapshot.sh" "$home") \
|| fail "selector-neutralized Bearings execution failed"
if liveness_property_holds "$mutated_json" "$id" "$held_id" "$active_id"; then
fail "neutralizing the measured-liveness selector left the guarded property green: $mutated_json"
fi
printf '%s' "$mutated_json" | jq -e --arg id "$id" --arg held "$held_id" --arg active "$active_id" '
(.gates | any(.id == $id) | not)
and (.gates | any(.id == $held and .reason == "captain route choice pending"))
and (.gates | any(.id == $active) | not)
' >/dev/null || fail "selector neutralization went RED for a reason other than removal of the phantom gate: $mutated_json"

set +e
run_teardown "$home" "$id" > "$home/teardown.out" 2> "$home/teardown.err"
Expand All @@ -90,7 +183,7 @@ EOF
[ "$rc" -ne 0 ] || fail "completed investigation teardown erased a report-only unresolved decision"
assert_present "$home/state/$id.meta" "refused completion must preserve investigation metadata"
assert_grep "REFUSED" "$home/teardown.err" "refusal must be explicit"
pass "report-only unresolved decision is reproduced and completion refuses before loss"
pass "verified-absent in-flight work gates while held and active workers avoid measured-liveness gates, selector neutralization goes RED, and completion refuses before report loss"
}

tasks_in() { # <home> <tasks-axi args...>
Expand Down
Loading