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
68 changes: 14 additions & 54 deletions bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
. "$SCRIPT_DIR/fm-classify-lib.sh"
# shellcheck source=bin/fm-busy-lib.sh
. "$SCRIPT_DIR/fm-busy-lib.sh"
# shellcheck source=bin/fm-nm-run-lib.sh
. "$SCRIPT_DIR/fm-nm-run-lib.sh"

ID=${1:-}
[ -n "$ID" ] || { echo "usage: fm-crew-state.sh <id>" >&2; exit 2; }
Expand Down Expand Up @@ -167,41 +169,20 @@ crew_busy_verdict() { # <target>
}

# --- no-mistakes run lookup (authoritative when a run matches this branch) --
# trim, strip_quotes, the bounded nm_run call, nm_field's TOON parse, and the
# branch+head attribution rule below are thin wrappers over the ONE owner in
# bin/fm-nm-run-lib.sh, shared with fm-teardown.sh's pre-teardown run abort.

trim() {
local s=${1:-}
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
strip_quotes() {
local s
s=$(trim "${1:-}")
case "$s" in
\"*\") s=${s#\"}; s=${s%\"} ;;
esac
trim "$s"
}

# Bounded no-mistakes call in the worktree; stdout only, never fails the script.
HAVE_TIMEOUT=none
if command -v timeout >/dev/null 2>&1; then HAVE_TIMEOUT=timeout
elif command -v gtimeout >/dev/null 2>&1; then HAVE_TIMEOUT=gtimeout
elif command -v perl >/dev/null 2>&1; then HAVE_TIMEOUT=perl
fi
trim() { fm_nm_trim "$@"; }
strip_quotes() { fm_nm_strip_quotes "$@"; }
nm_run() { # <args...>
case "$HAVE_TIMEOUT" in
timeout) ( cd "$WT" && timeout "$NM_TIMEOUT" no-mistakes "$@" ) 2>/dev/null || true ;;
gtimeout) ( cd "$WT" && gtimeout "$NM_TIMEOUT" no-mistakes "$@" ) 2>/dev/null || true ;;
perl) ( cd "$WT" && perl -e 'my $t = shift; my $pid = fork; die "fork failed" unless defined $pid; if (!$pid) { setpgrp(0, 0); exec @ARGV } local $SIG{ALRM} = sub { kill "TERM", -$pid; select undef, undef, undef, 0.2; kill "KILL", -$pid; exit 124 }; alarm $t; waitpid $pid, 0; exit($? >> 8)' "$NM_TIMEOUT" no-mistakes "$@" ) 2>/dev/null || true ;;
*) true ;;
esac
fm_nm_run "$WT" "$NM_TIMEOUT" "$@"
}

# Scalar value of a TOON key in the captured run output ($RUN_OUT).
RUN_OUT=""
nm_field() { # <key>
printf '%s\n' "$RUN_OUT" | sed -n "s/^[[:space:]]*$1:[[:space:]]*\(.*\)/\1/p" | head -1
fm_nm_field "$RUN_OUT" "$1"
}
# Finding count from a findings[N]{...} table header; empty when none.
nm_findings_count() {
Expand Down Expand Up @@ -385,40 +366,19 @@ nm_runs_status_for_branch() { # <branch>
CREW_BRANCH=$(git -C "$WT" symbolic-ref --quiet --short HEAD 2>/dev/null || true)

# 0 if the active axi-status run's head field matches this worktree's code
# identity. Branch match is a precondition (caller). Rules:
# - missing/empty head field: cannot bind; reject the run
# - equal commits (short or full SHA): match
# - worktree HEAD is an ancestor of run head: match (pipeline fix commits on
# the same history advanced the run tip)
# - run head is a strict ancestor of worktree HEAD: no match (local work
# advanced outside the run)
# - diverged / run head not in this worktree: no match (rewritten branch tip)
# identity. Branch match is a precondition (caller). Rule owned by
# fm_nm_head_matches_worktree in bin/fm-nm-run-lib.sh.
nm_run_head_matches_worktree() {
local run_head local_full run_full
local run_head
run_head=$(strip_quotes "$(nm_field head)")
[ -n "$run_head" ] || return 1
local_full=$(git -C "$WT" rev-parse HEAD 2>/dev/null) || return 1
run_full=$(git -C "$WT" rev-parse --verify "${run_head}^{commit}" 2>/dev/null) || return 1
[ "$run_full" = "$local_full" ] && return 0
if git -C "$WT" merge-base --is-ancestor "$local_full" "$run_full" 2>/dev/null; then
return 0
fi
return 1
fm_nm_head_matches_worktree "$WT" "$run_head"
}

# Coarse runs-list rows are "<status> <branch> <short-sha> ...". 0 if the short
# sha for this branch row matches the worktree head under the same rules as
# nm_run_head_matches_worktree (equal, or local is ancestor of run tip).
nm_coarse_head_matches_worktree() { # <short-sha>
local run_head=$1 local_full run_full
[ -n "$run_head" ] || return 1
local_full=$(git -C "$WT" rev-parse HEAD 2>/dev/null) || return 1
run_full=$(git -C "$WT" rev-parse --verify "${run_head}^{commit}" 2>/dev/null) || return 1
[ "$run_full" = "$local_full" ] && return 0
if git -C "$WT" merge-base --is-ancestor "$local_full" "$run_full" 2>/dev/null; then
return 0
fi
return 1
fm_nm_head_matches_worktree "$WT" "$1"
}

HAVE_RUN=0
Expand Down
73 changes: 73 additions & 0 deletions bin/fm-nm-run-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Shared no-mistakes axi run attribution primitives.
#
# ONE owner for the branch+code-identity matching rule that decides whether a
# no-mistakes run belongs to a given worktree, used by fm-crew-state.sh
# (read-only current-state reporting) and fm-teardown.sh (pre-teardown run
# abort, see its "Fix 1" header comment). Getting this wrong in either
# direction is unsafe: a false negative hides a genuinely parked run, and a
# false positive lets teardown act on a run it does not own.
#
# Bounded call to `no-mistakes "$@"` in dir $1, timeout $2 seconds. The bounded
# form preserves stdout, stderr, and exit status; the checked form discards
# stderr, while fm_nm_run keeps the fail-open query contract for read-only callers.
fm_nm_run_bounded() { # <dir> <timeout_secs> <args...>
local dir=$1 timeout_secs=$2 have_timeout=none
shift 2
if command -v timeout >/dev/null 2>&1; then have_timeout=timeout
elif command -v gtimeout >/dev/null 2>&1; then have_timeout=gtimeout
elif command -v perl >/dev/null 2>&1; then have_timeout=perl
fi
case "$have_timeout" in
timeout) ( cd "$dir" && timeout "$timeout_secs" no-mistakes "$@" ) ;;
gtimeout) ( cd "$dir" && gtimeout "$timeout_secs" no-mistakes "$@" ) ;;
perl) ( cd "$dir" && perl -e 'my $t = shift; my $pid = fork; die "fork failed" unless defined $pid; if (!$pid) { setpgrp(0, 0); exec @ARGV } local $SIG{ALRM} = sub { kill "TERM", -$pid; select undef, undef, undef, 0.2; kill "KILL", -$pid; exit 124 }; alarm $t; waitpid $pid, 0; exit($? >> 8)' "$timeout_secs" no-mistakes "$@" ) ;;
*) return 1 ;;
esac
}

fm_nm_run_checked() { # <dir> <timeout_secs> <args...>
fm_nm_run_bounded "$@" 2>/dev/null
}

fm_nm_run() { # <dir> <timeout_secs> <args...>
fm_nm_run_checked "$@" || true
}

fm_nm_trim() {
local s=${1:-}
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}

fm_nm_strip_quotes() {
local s
s=$(fm_nm_trim "${1:-}")
case "$s" in
\"*\") s=${s#\"}; s=${s%\"} ;;
esac
fm_nm_trim "$s"
}

# Scalar value of a TOON key in captured `axi status` output $1.
fm_nm_field() { # <toon-output> <key>
printf '%s\n' "$1" | sed -n "s/^[[:space:]]*$2:[[:space:]]*\(.*\)/\1/p" | head -1
}

# 0 if run head $2 matches worktree $1's code identity, per the same rule
# everywhere this attribution is needed:
# - missing/empty head: cannot bind; reject
# - equal commits (short or full SHA): match
# - worktree HEAD is an ancestor of run head: match (pipeline fix commits on
# the same history advanced the run tip past local HEAD)
# - run head is a strict ancestor of worktree HEAD, or diverged: no match
# (local work advanced outside the run, or the branch tip was rewritten)
fm_nm_head_matches_worktree() { # <worktree> <run_head>
local wt=$1 run_head=$2 local_full run_full
[ -n "$run_head" ] || return 1
local_full=$(git -C "$wt" rev-parse HEAD 2>/dev/null) || return 1
run_full=$(git -C "$wt" rev-parse --verify "${run_head}^{commit}" 2>/dev/null) || return 1
[ "$run_full" = "$local_full" ] && return 0
git -C "$wt" merge-base --is-ancestor "$local_full" "$run_full" 2>/dev/null
}
Loading
Loading