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
77 changes: 71 additions & 6 deletions .claude/scripts/loop-census.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
# hasn't reached PR stage. A tick uses this to
# avoid double-spawning an orchestrator for an
# issue that already has a worktree in progress.
# A REMOTE-ONLY branch match (no local branch of
# the same name) that turns out to be a stale
# ref for an issue whose PR already MERGED is
# ignored (branch=none) instead of counting as
# in_flight forever — see STALE-MERGED-REMOTE
# below (issue #158).
# stalled=<n> age_min=<m> one line PER in_flight issue whose most recent
# events.jsonl activity is older than the stall
# threshold (issue #98) — see STALL DETECTION
Expand Down Expand Up @@ -117,6 +123,26 @@
# one candidate was otherwise eligible; with zero eligible candidates,
# advance_ready stays "none" exactly as before this feature.
#
# --- STALE-MERGED-REMOTE (issue #158) ---------------------------------------
# `git branch -a --list "*feat/issue-N-*"` matches BOTH local branches and
# remote-tracking refs (`remotes/origin/...`). After a PR merges — even with
# `gh pr merge --delete-branch` — the MAIN checkout's local remote-tracking
# ref for that branch can survive until pruned (`git fetch --prune` /
# `git remote prune`). Without this guard, a still-open issue whose partial
# PR already merged would keep matching that stale ref FOREVER, so census
# would report it in_flight forever — wedging advance_ready and eventually
# tripping stall/escalation for an issue that in truth has no active branch.
#
# Fix: when the ONLY match for an issue is a remote-tracking ref (no local
# branch of the same name), look up whether a MERGED PR exists for that bare
# branch name. If one does, treat the issue as branch=none (ignore the stale
# ref) — it neither counts as in_flight nor blocks advance_ready. If no
# merged PR is found (the common case: work genuinely in progress, only
# pushed, local branch since deleted), remote-only in_flight is preserved
# exactly as before this fix. A genuine LOCAL branch match is unaffected —
# it always wins over a remote-only one, so this only changes behavior for
# the specific remote-only-and-already-merged case.
#
# --- STALL DETECTION (issue #98) --------------------------------------------
# An `in_flight` issue (feat/issue-N-* branch exists, no open PR yet) can sit
# forever if the driver that created it hung or died without ever reaching
Expand Down Expand Up @@ -348,12 +374,51 @@ while IFS=$'\t' read -r num labels title; do
done <<< "$module_labels"
[ "$hit" -eq 1 ] || continue
planned_count=$((planned_count + 1))
# Existing feat/issue-<n>-* branch (local or remote) means it's already in flight.
# NOTE: `| head -1` can make `git` see SIGPIPE (exit 141) if head closes the
# pipe before git finishes writing; under `set -euo pipefail` that would abort
# this whole script. `|| true` on the assignment absorbs that non-fatal
# pipeline failure — the captured output (head's one line) is unaffected.
branch=$(git -C "$root" branch -a --list "*feat/issue-$num-*" | head -1 | sed 's/^[* ]*//;s|^remotes/||') || true
# Existing feat/issue-<n>-* branch: a LOCAL branch means genuinely in_flight
# (branch #158) regardless of GitHub state. A REMOTE-ONLY match
# (`remotes/origin/...`, no local branch of the same name) is only treated
# as "existing" if it is NOT a stale ref left over from an already-merged
# PR (issue #158): `git branch -a --list` matches BOTH local branches and
# remote-tracking refs, and after a PR merges (even with
# `gh pr merge --delete-branch`) the MAIN checkout's local remote-tracking
# ref for it can survive until pruned — so a still-open issue whose partial
# PR already merged would otherwise match that stale ref FOREVER and never
# advance (wedges advance_ready, triggers false stall/escalation). A local
# match, when present, always wins over a remote-only one.
local_branch=""
remote_branch=""
branch_lines=$(git -C "$root" branch -a --list "*feat/issue-$num-*" | sed 's/^[* ]*//') || true
if [ -n "$branch_lines" ]; then
while IFS= read -r bl; do
[ -z "$bl" ] && continue
case "$bl" in
remotes/*) [ -z "$remote_branch" ] && remote_branch="$bl" ;;
*) [ -z "$local_branch" ] && local_branch="$bl" ;;
esac
done <<< "$branch_lines"
fi

if [ -n "$local_branch" ]; then
branch="$local_branch"
elif [ -n "$remote_branch" ]; then
# Bare branch name (strip the "remotes/<remote>/" prefix, e.g.
# "remotes/origin/feat/issue-158-x" -> "feat/issue-158-x") to query gh
# for a merged PR under it. Only done on this remote-only path — never on
# the common local-branch path above — to avoid the extra gh call there.
# Guarded with `|| true` (transient gh failure degrades gracefully to "no
# merged PR found", i.e. the prior remote-only-stays-in_flight behavior;
# it must never abort the census under `set -euo pipefail`).
bare_branch="${remote_branch#remotes/*/}"
merged_count=$(gh pr list -R "$repo" --state merged --head "$bare_branch" --json number --jq 'length' 2>/dev/null) || true
case "$merged_count" in ''|*[!0-9]*) merged_count=0 ;; esac
if [ "$merged_count" -ge 1 ]; then
branch="none" # stale remote-tracking ref for an already-merged PR — ignore it (issue #158)
else
branch="${remote_branch#remotes/}"
fi
else
branch="none"
fi
[ -n "$branch" ] || branch="none"
detail+="issue=$num branch=$branch title=$title"$'\n'

Expand Down
36 changes: 31 additions & 5 deletions .claude/scripts/loop-census.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ check() {
# - issue 100: a LOCAL branch feat/issue-100-w, which ALREADY has an open
# PR under its bare (exact, no prefix) name -> must NOT be
# in_flight (the plain exact-match control case).
# - issue 44: a REMOTE-tracking-only branch feat/issue-44-m (pushed, local
# copy deleted, same shape as issue 42), but a MERGED PR
# already exists for it -> stale-merged-remote (issue #158):
# must report branch=none and NOT be in_flight, even though
# the remote-tracking ref itself still exists.
# ---------------------------------------------------------------------------
fixture="$work/fixture1"
scripts_dir="$fixture/.claude/scripts"
Expand Down Expand Up @@ -122,18 +127,28 @@ EOF

# Fake bot-gh.sh: no network, no real `gh` — dispatches on the subcommand and
# a `--json` marker to canned, fixture-appropriate output.
# - `pr list --state merged --head <branch> ...`: merged-PR count for a
# bare branch name (issue #158) — 1 for feat/issue-44-m (the
# stale-merged-remote case), 0 for everything else (default). Checked
# BEFORE the headRefName/default branches below since it shares the `pr`
# subcommand but never carries the `headRefName` marker.
# - `pr list ... --json headRefName ...`: bare branch names of open PRs —
# issues 43 and 100 already have one; issue 42 does not (issue 4 has no
# branch, so it can't have a PR either).
# issues 43 and 100 already have one; issue 42 and 44 do not (issue 4 has
# no branch, so it can't have a PR either).
# - `pr list ... --json number ...`: open PR count (2, matching above).
# - `issue list ...`: TSV `num<TAB>labels<TAB>title`
# for the four planned+module:test issues.
# for the five planned+module:test issues.
cat > "$scripts_dir/bot-gh.sh" <<'EOF'
#!/usr/bin/env bash
case "$1" in
repo) echo "acme/repo" ;;
pr)
if printf '%s\n' "$*" | grep -q 'headRefName'; then
if printf '%s\n' "$*" | grep -q -- '--state merged'; then
case "$*" in
*"--head feat/issue-44-m"*) echo 1 ;;
*) echo 0 ;;
esac
elif printf '%s\n' "$*" | grep -q 'headRefName'; then
printf '%s\n' "feat/issue-43-z"
printf '%s\n' "feat/issue-100-w"
else
Expand All @@ -144,6 +159,7 @@ case "$1" in
printf '4\tplanned,module:test\tIssue four\n'
printf '42\tplanned,module:test\tIssue forty two\n'
printf '43\tplanned,module:test\tIssue forty three\n'
printf '44\tplanned,module:test\tIssue forty four\n'
printf '100\tplanned,module:test\tIssue one hundred\n'
;;
*) echo "fake-bot-gh.sh: unhandled args: $*" >&2; exit 1 ;;
Expand Down Expand Up @@ -193,6 +209,14 @@ git -C "$fixture" branch feat/issue-43-z main >/dev/null
git -C "$fixture" push -q origin feat/issue-43-z >/dev/null 2>&1
git -C "$fixture" branch -D feat/issue-43-z >/dev/null

# issue 44: same shape as 42/43 (remote-tracking-only ref, local copy
# deleted), but the fake bot-gh.sh reports a MERGED PR already exists for its
# bare branch name — the stale-merged-remote case (issue #158): this ref must
# be ignored (branch=none), not counted as in_flight forever.
git -C "$fixture" branch feat/issue-44-m main >/dev/null
git -C "$fixture" push -q origin feat/issue-44-m >/dev/null 2>&1
git -C "$fixture" branch -D feat/issue-44-m >/dev/null

# issue 100: LOCAL-only branch (never pushed) — exact-match control.
git -C "$fixture" branch feat/issue-100-w main >/dev/null

Expand All @@ -208,8 +232,10 @@ check "issue 4 is NOT in_flight (no branch to be in flight with)" bash -c '! pri
check "issue 42 (remote-only branch, no open PR) IS in_flight" bash -c 'printf "%s\n" "$1" | grep -qx "in_flight=42"' _ "$out"
check "issue 43 (remote-only branch, already has an open PR via origin/ strip+suffix match) is NOT in_flight" bash -c '! printf "%s\n" "$1" | grep -qx "in_flight=43"' _ "$out"
check "issue 100 (local branch, already has an open PR, exact-match control) is NOT in_flight" bash -c '! printf "%s\n" "$1" | grep -qx "in_flight=100"' _ "$out"
check "issue 44 (remote-only branch, MERGED PR already exists) reports branch=none (issue #158)" bash -c 'printf "%s\n" "$1" | grep -q "^issue=44 branch=none"' _ "$out"
check "issue 44 (stale merged remote-only ref) is NOT in_flight (issue #158)" bash -c '! printf "%s\n" "$1" | grep -qx "in_flight=44"' _ "$out"
check "exactly one in_flight line total (only issue 42 qualifies)" bash -c '[ "$(printf "%s\n" "$1" | grep -c "^in_flight=")" -eq 1 ]' _ "$out"
check "planned_issues=4 counted" bash -c 'printf "%s\n" "$1" | grep -qx "planned_issues=4"' _ "$out"
check "planned_issues=5 counted" bash -c 'printf "%s\n" "$1" | grep -qx "planned_issues=5"' _ "$out"
check "issue=42 branch line shows the origin-prefixed remote-tracking name" bash -c 'printf "%s\n" "$1" | grep -q "^issue=42 branch=origin/feat/issue-42-y"' _ "$out"
check "ci_fix_prs=0 counted (no-op pr-ci-fix.sh stub, issue #96)" bash -c 'printf "%s\n" "$1" | grep -qx "ci_fix_prs=0"' _ "$out"
check "comment_fix_prs=0 counted (no-op pr-comment-fix.sh stub, issue #96 part 2)" bash -c 'printf "%s\n" "$1" | grep -qx "comment_fix_prs=0"' _ "$out"
Expand Down
Loading