From c5e28b19844a1a6de079c2801fccf71316d53147 Mon Sep 17 00:00:00 2001 From: codejeet Date: Sat, 25 Apr 2026 10:25:23 +0200 Subject: [PATCH] Prevent refill recovery from trusting stale authorization and worker state Witness and mayor recovery can see transient GitHub label read failures and stale Ralph task state at the same time. This keeps resling authorization tied to live fallback evidence, records concrete auth/read failures, and makes active Ralph lanes require a live polecat issue instead of task metadata alone. Constraint: PMKB canonical refill produced open sgt-authorized issues whose reslings were falsely skipped as unauthorized while rig activity still showed dead workers as active. Rejected: Treat empty label output as unauthorized | it hides GraphQL/API read failures and recreates RESLING_SKIP_UNAUTHORIZED drift. Confidence: high Scope-risk: moderate Directive: Do not count Ralph active lanes from plan task status alone; require live polecat evidence for the issue number. Tested: bash -n sgt; focused witness/resling/Ralph/status regression bundle Not-tested: Live PMKB GitHub refill run --- sgt | 304 ++++++++++++++++++-- test_ralph_live_polecat_lane_accounting.sh | 121 ++++++++ test_ralph_refill_attach_blocker.sh | 5 + test_resling_authorization_fallback.sh | 69 +++++ test_status_json.sh | 4 +- test_status_rig_activity_live_accounting.sh | 100 +++++++ test_sweep_backend_limited_skip.sh | 3 + test_witness_alive_stall_classification.sh | 15 + test_witness_codex_usage_limit_block.sh | 19 ++ test_witness_manual_hibernation_skip.sh | 15 + test_witness_stalled_followup_guard.sh | 14 + 11 files changed, 638 insertions(+), 31 deletions(-) create mode 100755 test_ralph_live_polecat_lane_accounting.sh create mode 100755 test_resling_authorization_fallback.sh create mode 100755 test_status_rig_activity_live_accounting.sh diff --git a/sgt b/sgt index b021f4c..c2fe70e 100755 --- a/sgt +++ b/sgt @@ -602,6 +602,8 @@ _REFINERY_MERGE_RECEIPT_VERIFIED_AT="" _REFINERY_MERGE_RECEIPT_OUTCOME="" _REFINERY_MERGE_RECEIPT_RETRY_DECISION="" _RESLING_LAST_POLECAT="" +_RESLING_LAST_FAILURE_REASON_CODE="" +_RESLING_LAST_FAILURE_DETAIL="" _RESLING_ISSUE_CLAIM_FILE="" _SGT_LAST_SLING_ISSUE_NUMBER="" _SGT_LAST_SLING_ISSUE_URL="" @@ -2272,6 +2274,39 @@ _mayor_rig_activity_ensure_state() { fi } +_mayor_rig_activity_status_fields() { + local rig="${1:-}" + local state reason changed_at changed_epoch mode meaningful_at meaningful_epoch meaningful_reason wake_at wake_reason + local live_snapshot live_state live_reason live_open_authorized live_open_prs live_active_polecats live_merge_queue live_pending_plan_requests live_plan_rollup live_plan_status + + _mayor_rig_activity_ensure_state "$rig" + IFS='|' read -r state reason changed_at changed_epoch mode meaningful_at meaningful_epoch meaningful_reason wake_at wake_reason <<< "$(_mayor_rig_activity_state_read "$rig")" + + live_snapshot="$(_mayor_rig_activity_snapshot "$rig" 2>/dev/null || true)" + if [[ "$live_snapshot" == *"|"* && "${mode:-none}" != "manual" ]]; then + IFS='|' read -r live_state live_reason live_open_authorized live_open_prs live_active_polecats live_merge_queue live_pending_plan_requests live_plan_rollup live_plan_status <<< "$live_snapshot" + if [[ "$state" != "hibernated" || "${mode:-none}" == "none" || "$live_state" == "active" ]]; then + state="$live_state" + reason="$live_reason" + if [[ "$state" != "hibernated" ]]; then + mode="none" + fi + fi + fi + + printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ + "${state:-unknown}" \ + "${reason:-}" \ + "${changed_at:-}" \ + "${changed_epoch:-}" \ + "${mode:-none}" \ + "${meaningful_at:-}" \ + "${meaningful_epoch:-}" \ + "${meaningful_reason:-}" \ + "${wake_at:-}" \ + "${wake_reason:-}" +} + _mayor_rig_set_manual_hibernation() { local rig="${1:-}" mode="${2:-hibernate}" reason="${3:-}" local prior state last_reason changed_at changed_epoch prior_mode last_meaningful_at last_meaningful_epoch last_meaningful_reason last_wake_at last_wake_reason @@ -2999,6 +3034,58 @@ _cleanup_failed_polecat_dispatch() { fi } +_polecat_issue_title() { + local cached_title="${1:-}" repo="${2:-}" issue="${3:-}" + local live_title="" + if [[ -n "$cached_title" ]]; then + printf '%s\n' "$cached_title" + return 0 + fi + [[ -n "$repo" && "$issue" =~ ^[0-9]+$ && "$issue" != "0" ]] || return 1 + live_title="$(_forge_issue_field "$(_workflow_backend_default)" "$repo" "$issue" "title" 2>/dev/null || true)" + [[ -n "$live_title" ]] || return 1 + printf '%s\n' "$live_title" +} + +_record_refill_attach_blocker() { + local rig="${1:-}" repo="${2:-}" issue_number="${3:-}" issue_title="${4:-}" source_event="${5:-unknown}" reason_code="${6:-dispatch-failed}" detail="${7:-dispatch failed}" + local requester title evidence blocker_id owner_repo issue_url comment_body + + [[ -n "$rig" && -n "$repo" && "$issue_number" =~ ^[0-9]+$ && "$issue_number" != "0" ]] || return 1 + + requester="witness" + owner_repo="$(_repo_owner_repo "$repo")" + issue_url="$(_repo_issue_url "$repo" "$issue_number")" + title="Refill could not attach replacement polecat for issue #$issue_number" + printf -v evidence '%s\n\nSGT could not attach a replacement polecat during refill/recovery.\n\n- Rig: %s\n- Repo: %s\n- Issue: #%s\n- Issue URL: %s\n- Issue title: %s\n- Source event: %s\n- Failure reason: %s\n- Detail: %s\n\nRequired follow-up:\n- investigate the spawn/worktree failure before repeating wake/refresh churn\n- do not treat the rig as healthy until a replacement polecat attaches successfully\n' \ + "$title" \ + "$rig" \ + "${owner_repo:-$repo}" \ + "$issue_number" \ + "${issue_url:-unknown}" \ + "${issue_title:-unknown}" \ + "${source_event:-unknown}" \ + "${reason_code:-unknown}" \ + "${detail:-unknown}" + + blocker_id="$(_acceptance_blocker_write "$rig" "$evidence" "$requester" "$reason_code" 2>/dev/null || true)" + if [[ -n "$blocker_id" ]]; then + _context_append_acceptance_blocker "$rig" "$blocker_id" "$requester" "$title" "$evidence" || true + if [[ -n "${OPENAI_API_KEY:-}" ]] && [[ -n "$(_context_python_bin)" ]]; then + _context_index_build "$rig" >/dev/null 2>&1 || true + fi + _wake_mayor "acceptance-blocker:${rig}:${blocker_id}" + log_event "REFILL_ATTACH_BLOCKER rig=$rig issue=#$issue_number blocker_id=$blocker_id source_event=$source_event reason_code=$reason_code detail=\"$(_escape_quotes "${detail:-unknown}")\"" + comment_body="[sgt] Refill could not attach a replacement polecat for issue #$issue_number. Recorded acceptance blocker \`$blocker_id\` with reason \`$reason_code\`: ${detail:-unknown}" + else + _wake_mayor "refill-attach-failed:${rig}:#${issue_number}" + log_event "REFILL_ATTACH_BLOCKER rig=$rig issue=#$issue_number blocker_id=unavailable source_event=$source_event reason_code=$reason_code detail=\"$(_escape_quotes "${detail:-unknown}")\"" + comment_body="[sgt] Refill could not attach a replacement polecat for issue #$issue_number. Mayor has been notified. Reason \`$reason_code\`: ${detail:-unknown}" + fi + _forge_issue_comment_add "$(_workflow_backend_default)" "$repo" "$issue_number" "$comment_body" >/dev/null 2>&1 || true + return 0 +} + _mayor_cleanup_stale_polecat() { local pfile="${1:-}" pname="${2:-}" rig="${3:-}" repo="${4:-}" issue="${5:-}" branch="${6:-}" session="${7:-}" worktree="${8:-}" reason_code="${9:-}" issue_state="${10:-}" pr_number="${11:-}" pr_state="${12:-}" local owner_repo key key_id fence_dir cleanup_marker decision_marker action @@ -3170,7 +3257,7 @@ _polecat_counts_as_active() { fi fi - if [[ -n "$worktree" && ! -d "$worktree" ]]; then + if [[ "$status" == "running" && -n "$worktree" && ! -d "$worktree" ]]; then rm -f "$pfile" 2>/dev/null || true log_event "POLECAT_COUNT_AUTO_PRUNE polecat=$pname reason_code=dead-session-missing-worktree rig=${rig:-unknown} issue=#${issue:-unknown} branch=\"$(_escape_quotes "$branch")\"" fi @@ -3458,6 +3545,35 @@ _active_polecat_count() { echo "$count" } +_active_polecat_issue_numbers() { + local rig="${1:-${SGT_MAYOR_SCOPE_RIG:-}}" + [[ -d "$SGT_POLECATS" ]] || return 0 + + local pf snapshot p_rig issue + local -A seen=() + for pf in "$SGT_POLECATS"/*; do + [[ -f "$pf" ]] || continue + if ! _polecat_counts_as_active "$pf" "$rig"; then + continue + fi + snapshot="$( + ( + source "$pf" + printf '%s|%s\n' "${RIG:-}" "${ISSUE:-}" + ) 2>/dev/null + )" + IFS='|' read -r p_rig issue <<< "$snapshot" + if [[ -n "$rig" && -n "$p_rig" && "$p_rig" != "$rig" ]]; then + continue + fi + [[ "$issue" =~ ^[0-9]+$ && "$issue" != "0" ]] || continue + if [[ -z "${seen[$issue]+x}" ]]; then + seen["$issue"]=1 + printf '%s\n' "$issue" + fi + done +} + _mayor_merge_queue_count_for_rig() { local rig="${1:-}" local mq_count=0 @@ -5117,8 +5233,13 @@ _refinery_conflict_resling_resume() { skip_reason="active polecat already exists for issue" _merge_queue_set_field "$evidence_file" "RESLING_DISPATCHED_POLECAT" "$existing_polecat" || true elif ! _has_sgt_authorized "$repo" "$issue"; then - skip_status="SKIPPED_UNAUTHORIZED" - skip_reason="issue lacks sgt-authorized label" + if [[ "${_SGT_AUTHZ_LAST_REASON_CODE:-}" == "label-read-failed" ]]; then + skip_status="SKIPPED_AUTHZ_READ_FAILED" + skip_reason="${_SGT_AUTHZ_LAST_DETAIL:-unable to verify sgt-authorized label}" + else + skip_status="SKIPPED_UNAUTHORIZED" + skip_reason="issue lacks sgt-authorized label (${_SGT_AUTHZ_LAST_DETAIL:-no detail})" + fi elif ! stale_reason=$(_resling_pre_dispatch_revalidate "$rig" "$repo" "$issue" "$source_pr"); then skip_status="SKIPPED_STALE" skip_reason="$stale_reason" @@ -6024,7 +6145,7 @@ _ralph_support_label() { } _plan_ralph_snapshot() { - local rig="${1:-}" plan_file="${2:-}" state_file="${3:-}" + local rig="${1:-}" plan_file="${2:-}" state_file="${3:-}" live_issue_numbers [[ -n "$rig" ]] || return 1 [[ -n "$plan_file" ]] || plan_file="$(_plan_file_path "$rig")" [[ -n "$state_file" ]] || state_file="$(_plan_state_path "$rig")" @@ -6032,12 +6153,18 @@ _plan_ralph_snapshot() { printf '0|0||0|0|0|task-backed unique non-support lanes only\n' return 0 fi - python3 - "$plan_file" "$state_file" <<'PY' + live_issue_numbers="$(_active_polecat_issue_numbers "$rig" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)" + python3 - "$plan_file" "$state_file" "$live_issue_numbers" <<'PY' import json import os import sys -plan_file, state_file = sys.argv[1:3] +plan_file, state_file, live_issue_numbers = sys.argv[1:4] +live_active_issues = { + issue.strip() + for issue in live_issue_numbers.split(",") + if issue.strip() +} with open(plan_file, "r", encoding="utf-8") as fh: plan = json.load(fh) @@ -6114,6 +6241,9 @@ for task in tasks: task_status = str(state_entry.get("status") or "").strip() if task_status not in {"dispatched", "in_progress"}: continue + issue_number = str(state_entry.get("issue_number") or task.get("issue_number") or "").strip() + if not issue_number or issue_number not in live_active_issues: + continue labels = task.get("labels") if not isinstance(labels, list): labels = [] @@ -6121,7 +6251,7 @@ for task in tasks: support_only = bool(task.get("support_only")) or task.get("ralph_lane") is False or bool(labels & support_labels) if support_only: continue - lane_key = str(task.get("ralph_lane_key") or state_entry.get("issue_number") or task_id).strip() + lane_key = str(task.get("ralph_lane_key") or issue_number).strip() if not lane_key or lane_key in counted: continue counted.add(lane_key) @@ -10237,6 +10367,23 @@ _gh_issue_labels_live() { printf '%s\n' "$labels" } +_SGT_AUTHZ_LAST_REASON_CODE="" +_SGT_AUTHZ_LAST_DETAIL="" + +_labels_contain_sgt_authorized() { + local labels="${1:-}" + printf '%s\n' "$labels" | grep -Fxq "sgt-authorized" +} + +_label_list_one_line() { + local labels="${1:-}" + if [[ -z "$labels" ]]; then + printf '%s\n' "none" + return 0 + fi + printf '%s\n' "$labels" | paste -sd ',' - +} + _gh_pr_state_mergeable_live() { local repo="${1:-}" pr="${2:-}" snapshot owner_repo backend state mergeable [[ -n "$repo" && -n "$pr" ]] || return 1 @@ -10263,13 +10410,65 @@ _gh_pr_state_mergeable_live() { # Security gate can be disabled by setting: SGT_REQUIRE_AUTH_LABEL=0 _has_sgt_authorized() { local repo="$1" issue="$2" + local backend labels rest_labels rest_error rest_rc list_hit list_error list_rc owner_repo + _SGT_AUTHZ_LAST_REASON_CODE="" + _SGT_AUTHZ_LAST_DETAIL="" if [[ "${SGT_REQUIRE_AUTH_LABEL:-1}" == "0" ]]; then + _SGT_AUTHZ_LAST_REASON_CODE="auth-label-disabled" + _SGT_AUTHZ_LAST_DETAIL="SGT_REQUIRE_AUTH_LABEL=0" return 0 fi - [[ -n "$issue" && "$issue" != "0" ]] || return 1 - local labels + if [[ ! "$issue" =~ ^[0-9]+$ || "$issue" == "0" ]]; then + _SGT_AUTHZ_LAST_REASON_CODE="invalid-issue" + _SGT_AUTHZ_LAST_DETAIL="issue number is missing or invalid" + return 1 + fi + + backend="$(_workflow_backend_default)" labels=$(_gh_issue_labels_live "$repo" "$issue" 2>/dev/null || true) - echo "$labels" | grep -qx "sgt-authorized" + if _labels_contain_sgt_authorized "$labels"; then + _SGT_AUTHZ_LAST_REASON_CODE="authorized" + _SGT_AUTHZ_LAST_DETAIL="label-source=${backend}" + return 0 + fi + + if [[ "$backend" != "github" ]]; then + _SGT_AUTHZ_LAST_REASON_CODE="label-missing" + _SGT_AUTHZ_LAST_DETAIL="labels=$(_label_list_one_line "$labels")" + return 1 + fi + + owner_repo="$(_repo_owner_repo "$repo")" + [[ -n "$owner_repo" ]] || owner_repo="$repo" + if rest_labels="$(gh api "repos/$owner_repo/issues/$issue" --jq '.labels[]?.name // empty' 2>&1)"; then + if _labels_contain_sgt_authorized "$rest_labels"; then + _SGT_AUTHZ_LAST_REASON_CODE="authorized" + _SGT_AUTHZ_LAST_DETAIL="label-source=github-rest labels=$(_label_list_one_line "$rest_labels")" + return 0 + fi + _SGT_AUTHZ_LAST_REASON_CODE="label-missing" + _SGT_AUTHZ_LAST_DETAIL="label-source=github-rest labels=$(_label_list_one_line "$rest_labels")" + return 1 + else + rest_rc=$? + fi + rest_error="$(_one_line "$rest_labels")" + + if list_hit="$(gh issue list --repo "$repo" --state all --label "sgt-authorized" --limit 1000 --json number --jq ".[] | select(.number == $issue) | .number" 2>&1)"; then + list_rc=0 + else + list_rc=$? + fi + if [[ "$list_rc" -eq 0 && "$list_hit" == "$issue" ]]; then + _SGT_AUTHZ_LAST_REASON_CODE="authorized" + _SGT_AUTHZ_LAST_DETAIL="label-source=authorized-issue-list" + return 0 + fi + list_error="$(_one_line "$list_hit")" + + _SGT_AUTHZ_LAST_REASON_CODE="label-read-failed" + _SGT_AUTHZ_LAST_DETAIL="initial_labels=$(_label_list_one_line "$labels"); rest_error=${rest_error:-empty}; list_error=${list_error:-empty}" + return 1 } _issue_has_label() { @@ -10610,8 +10809,15 @@ cmd_sling() { local worktree="$SGT_ROOT/polecats/$pname" mkdir -p "$SGT_ROOT/polecats" - git -C "$rpath" worktree add -b "$branch" "$worktree" "origin/$default_branch" 2>/dev/null \ - || git -C "$rpath" worktree add -b "$branch" "$worktree" "$default_branch" + local worktree_error="" + if ! worktree_error="$( + git -C "$rpath" worktree add -b "$branch" "$worktree" "origin/$default_branch" 2>&1 \ + || git -C "$rpath" worktree add -b "$branch" "$worktree" "$default_branch" 2>&1 + )"; then + _cleanup_failed_polecat_dispatch "$SGT_POLECATS/$pname" "$pname" "$rig" "$branch" "$session_name" "$worktree" + log_event "SLING_SPAWN_FAILED polecat=$pname rig=$rig issue=#$issue_number branch=$branch reason_code=worktree-attach-failed detail=\"$(_escape_quotes "${worktree_error:-git worktree add returned non-zero exit status}")\"" + die "failed to attach worktree for polecat '$pname': ${worktree_error:-git worktree add returned non-zero exit status}" + fi _ensure_context_file "$rig" >/dev/null if [[ -f "$rpath/CLAUDE.md" ]]; then @@ -10628,6 +10834,7 @@ RIG=$rig REPO=$repo ISSUE=$issue_number ISSUE_URL=$issue_url +ISSUE_TITLE=$(printf '%q' "$task") BRANCH=$branch WORKTREE=$worktree OUTPUT_LOG=$worktree/.sgt-agent-output.log @@ -10823,9 +11030,8 @@ cmd_status_json() { agents+=("{\"name\":$(_json_quote "witness/$rig_name"),\"role\":$(_json_quote "witness"),\"scope\":$(_json_quote "rig"),\"rig\":$(_json_quote "$rig_name"),\"status\":$(_json_quote "$w_alive")}") agents+=("{\"name\":$(_json_quote "refinery/$rig_name"),\"role\":$(_json_quote "refinery"),\"scope\":$(_json_quote "rig"),\"rig\":$(_json_quote "$rig_name"),\"status\":$(_json_quote "$r_alive")}") if _mayor_rig_activity_enabled; then - _mayor_rig_activity_ensure_state "$rig_name" local mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason - IFS='|' read -r mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason <<< "$(_mayor_rig_activity_state_read "$rig_name")" + IFS='|' read -r mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason <<< "$(_mayor_rig_activity_status_fields "$rig_name")" mayor_rigs+=("{\"rig\":$(_json_quote "$rig_name"),\"state\":$(_json_quote "$mr_state"),\"reason\":$(_json_quote "$mr_reason"),\"changed_at\":$(_json_quote "$mr_changed_at"),\"changed_epoch\":$(_json_quote "$mr_changed_epoch"),\"hibernation_mode\":$(_json_quote "$mr_mode"),\"last_meaningful_at\":$(_json_quote "$mr_meaningful_at"),\"last_meaningful_epoch\":$(_json_quote "$mr_meaningful_epoch"),\"last_meaningful_reason\":$(_json_quote "$mr_meaningful_reason"),\"last_wake_at\":$(_json_quote "$mr_wake_at"),\"last_wake_reason\":$(_json_quote "$mr_wake_reason")}") fi done @@ -11379,8 +11585,7 @@ _cmd_status_human() { mayor_rig_count=$((mayor_rig_count + 1)) local rig_name mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason rig_name="$(basename "$rig_file")" - _mayor_rig_activity_ensure_state "$rig_name" - IFS='|' read -r mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason <<< "$(_mayor_rig_activity_state_read "$rig_name")" + IFS='|' read -r mr_state mr_reason mr_changed_at mr_changed_epoch mr_mode mr_meaningful_at mr_meaningful_epoch mr_meaningful_reason mr_wake_at mr_wake_reason <<< "$(_mayor_rig_activity_status_fields "$rig_name")" printf " %-16s %s %s%s%s\n" "$rig_name" "$(_status_badge "$mr_state")" "$(_dim)" "mode=${mr_mode:-none}" "$(_reset)" if [[ -n "$mr_reason" ]]; then printf " %-16s %s%s%s\n" "" "$(_dim)" "reason: $mr_reason" "$(_reset)" @@ -11962,12 +12167,14 @@ _mayor_recover_stranded_actionable_rig() { resling_output="$(cat "$resling_log" 2>/dev/null || true)" rm -f "$resling_log" - blocked_reason_code="dispatch-failed" - blocked_detail="$(_one_line "${resling_output:-dispatch failed}")" + blocked_reason_code="${_RESLING_LAST_FAILURE_REASON_CODE:-dispatch-failed}" + blocked_detail="$(_one_line "${_RESLING_LAST_FAILURE_DETAIL:-${resling_output:-dispatch failed}}")" if [[ "$blocked_detail" == *"dispatch-instant gate"* ]]; then blocked_reason_code="final-gate" elif [[ "$blocked_detail" == *"stale event"* ]]; then blocked_reason_code="stale-event" + elif [[ "$blocked_detail" == *"unable to verify sgt-authorized label"* ]]; then + blocked_reason_code="authz-read-failed" elif [[ "$blocked_detail" == *"lacks sgt-authorized label"* ]]; then blocked_reason_code="unauthorized" elif [[ "$blocked_detail" == *"backend-limited"* ]]; then @@ -11979,6 +12186,9 @@ _mayor_recover_stranded_actionable_rig() { _MAYOR_STRANDED_RECOVERY_DETAILS+="${issue_number}|${blocked_reason_code}|${blocked_detail}"$'\n' echo "[mayor] stranded rig $rig could not recover issue #$issue_number ($blocked_reason_code)" log_event "MAYOR_STRANDED_RIG_RECOVERY_BLOCKED issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=$blocked_reason_code detail=\"$(_escape_quotes "$blocked_detail")\" source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\"" + if [[ "$blocked_reason_code" == "worktree-attach-failed" || "$blocked_reason_code" == "tmux-new-session-failed" || "$blocked_reason_code" == "command-too-long" || "$blocked_reason_code" == "label-read-failed" || "$blocked_reason_code" == "authz-read-failed" ]]; then + _record_refill_attach_blocker "$rig" "$repo" "$issue_number" "$issue_title" "$source_event" "$blocked_reason_code" "$blocked_detail" || true + fi done <<< "$issue_rows" } @@ -12922,12 +13132,13 @@ _witness_loop() { pname="$(basename "$f")" # Source in subshell to avoid polluting - local p_session p_branch p_issue p_worktree p_created p_repo p_auto_merge p_output_log + local p_session p_branch p_issue p_issue_title p_worktree p_created p_repo p_auto_merge p_output_log local p_runtime_classification p_runtime_reason_code p_runtime_summary p_runtime_output_age p_runtime_busy_pid p_runtime_busy_comm - eval "$(grep -E '^(SESSION|BRANCH|ISSUE|WORKTREE|CREATED|REPO|AUTO_MERGE|BACKEND|OUTPUT_LOG|RUNTIME_CLASSIFICATION|RUNTIME_REASON_CODE|RUNTIME_SUMMARY|RUNTIME_OUTPUT_AGE_SECS|RUNTIME_BUSY_PID|RUNTIME_BUSY_COMM)=' "$f")" + eval "$(grep -E '^(SESSION|BRANCH|ISSUE|ISSUE_TITLE|WORKTREE|CREATED|REPO|AUTO_MERGE|BACKEND|OUTPUT_LOG|RUNTIME_CLASSIFICATION|RUNTIME_REASON_CODE|RUNTIME_SUMMARY|RUNTIME_OUTPUT_AGE_SECS|RUNTIME_BUSY_PID|RUNTIME_BUSY_COMM)=' "$f")" p_session="$SESSION" p_branch="$BRANCH" p_issue="$ISSUE" + p_issue_title="${ISSUE_TITLE:-}" p_worktree="$WORKTREE" p_created="$CREATED" p_repo="$REPO" @@ -13014,7 +13225,7 @@ _witness_loop() { rm -f "$f" local issue_title - issue_title="$(_forge_issue_field "$workflow_backend" "$p_repo" "$p_issue" "title" 2>/dev/null || true)" + issue_title="$(_polecat_issue_title "$p_issue_title" "$p_repo" "$p_issue" 2>/dev/null || true)" if [[ -n "$issue_title" ]]; then if _mayor_rig_manually_hibernated "$rig"; then echo "[witness/$rig] rig manually hibernated — leaving stalled issue #$p_issue parked" @@ -13090,7 +13301,7 @@ _witness_loop() { rm -f "$f" local issue_title - issue_title="$(_forge_issue_field "$workflow_backend" "$p_repo" "$p_issue" "title" 2>/dev/null || true)" + issue_title="$(_polecat_issue_title "$p_issue_title" "$p_repo" "$p_issue" 2>/dev/null || true)" _witness_record_backend_limit_followup "$rig" "$p_repo" "$p_issue" "$issue_title" "$pname" "${backend_limit_name:-${BACKEND:-unknown}}" "$backend_limit_reason" "$backend_limit_match" continue fi @@ -13114,7 +13325,7 @@ _witness_loop() { # Re-sling: get the issue title and re-dispatch local issue_title - issue_title="$(_forge_issue_field "$workflow_backend" "$p_repo" "$p_issue" "title" 2>/dev/null || true)" + issue_title="$(_polecat_issue_title "$p_issue_title" "$p_repo" "$p_issue" 2>/dev/null || true)" if [[ -n "$issue_title" ]]; then if _mayor_rig_manually_hibernated "$rig"; then echo "[witness/$rig] rig manually hibernated — leaving stalled issue #$p_issue parked" @@ -13219,11 +13430,15 @@ _resling_existing_issue() { local source_event="${7:-unknown}" local source_event_key="${8:-$source_event}" local canonical_repo resolve_meta resolve_code resolve_reason + _RESLING_LAST_FAILURE_REASON_CODE="" + _RESLING_LAST_FAILURE_DETAIL="" source_event_key="$(_wake_trigger_key "$source_event_key")" [[ -n "$source_event_key" ]] || source_event_key="${source_event:-unknown}" if _mayor_rig_manually_hibernated "$rig"; then echo "[resling] rig $rig is manually hibernated — skipping issue #$issue_number" log_event "RESLING_SKIP_HIBERNATED issue=#$issue_number rig=$rig mode=manual source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\"" + _RESLING_LAST_FAILURE_REASON_CODE="manual-hibernation" + _RESLING_LAST_FAILURE_DETAIL="rig $rig is manually hibernated" return 1 fi @@ -13232,6 +13447,8 @@ _resling_existing_issue() { IFS='|' read -r resolve_code resolve_reason <<< "$resolve_meta" echo "[resling] stale event ($source_event) for issue #$issue_number — skipping: ${resolve_reason:-unable to resolve rig repo}" log_event "RESLING_SKIP_STALE issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} skip_reason=\"$(_escape_quotes "${resolve_reason:-unable to resolve rig repo}")\"" + _RESLING_LAST_FAILURE_REASON_CODE="${resolve_code:-stale-event}" + _RESLING_LAST_FAILURE_DETAIL="${resolve_reason:-unable to resolve rig repo}" return 1 fi repo="$canonical_repo" @@ -13240,13 +13457,24 @@ _resling_existing_issue() { if ! stale_reason=$(_resling_pre_dispatch_revalidate "$rig" "$repo" "$issue_number" "$source_pr"); then echo "[resling] stale event ($source_event) for issue #$issue_number — skipping: $stale_reason" log_event "RESLING_SKIP_STALE issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} skip_reason=\"$(_escape_quotes "$stale_reason")\"" + _RESLING_LAST_FAILURE_REASON_CODE="stale-event" + _RESLING_LAST_FAILURE_DETAIL="$stale_reason" return 1 fi # Security gate: verify issue has sgt-authorized label if ! _has_sgt_authorized "$repo" "$issue_number"; then - echo "[resling] issue #$issue_number lacks sgt-authorized label — skipping" - log_event "RESLING_SKIP_UNAUTHORIZED issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" skip_reason=\"issue lacks sgt-authorized\"" + local authz_reason_code="${_SGT_AUTHZ_LAST_REASON_CODE:-unauthorized}" + local authz_detail="${_SGT_AUTHZ_LAST_DETAIL:-issue lacks sgt-authorized}" + if [[ "$authz_reason_code" == "label-read-failed" ]]; then + echo "[resling] unable to verify sgt-authorized label for issue #$issue_number — skipping: $authz_detail" + log_event "RESLING_SKIP_AUTHZ_READ_FAILED issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" reason_code=$authz_reason_code detail=\"$(_escape_quotes "$authz_detail")\"" + else + echo "[resling] issue #$issue_number lacks sgt-authorized label — skipping" + log_event "RESLING_SKIP_UNAUTHORIZED issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" reason_code=$authz_reason_code detail=\"$(_escape_quotes "$authz_detail")\" skip_reason=\"issue lacks sgt-authorized\"" + fi + _RESLING_LAST_FAILURE_REASON_CODE="$authz_reason_code" + _RESLING_LAST_FAILURE_DETAIL="$authz_detail" return 1 fi @@ -13254,6 +13482,8 @@ _resling_existing_issue() { if backend_limited_reason="$(_issue_backend_dispatch_limited_reason "$repo" "$issue_number" 2>/dev/null || true)" && [[ -n "$backend_limited_reason" ]]; then echo "[resling] issue #$issue_number is backend-limited — skipping" log_event "RESLING_SKIP_BACKEND_LIMIT issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} reason_code=$backend_limited_reason" + _RESLING_LAST_FAILURE_REASON_CODE="$backend_limited_reason" + _RESLING_LAST_FAILURE_DETAIL="issue #$issue_number is backend-limited" return 1 fi @@ -13261,6 +13491,8 @@ _resling_existing_issue() { if existing_issue_polecat=$(_resling_find_existing_issue_polecat "$rig" "$repo" "$issue_number"); then echo "[resling] issue #$issue_number already has active polecat $existing_issue_polecat — skipping duplicate re-sling" log_event "RESLING_SKIP_DUPLICATE_ACTIVE issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} existing_polecat=$existing_issue_polecat skip_reason=\"active polecat already exists for issue\"" + _RESLING_LAST_FAILURE_REASON_CODE="active-polecat-existing" + _RESLING_LAST_FAILURE_DETAIL="issue #$issue_number already has active polecat $existing_issue_polecat" return 1 fi @@ -13277,8 +13509,18 @@ _resling_existing_issue() { local worktree="$SGT_ROOT/polecats/$pname" mkdir -p "$SGT_ROOT/polecats" - git -C "$rpath" worktree add -b "$branch" "$worktree" "origin/$default_branch" 2>/dev/null \ - || git -C "$rpath" worktree add -b "$branch" "$worktree" "$default_branch" + local worktree_error="" + if ! worktree_error="$( + git -C "$rpath" worktree add -b "$branch" "$worktree" "origin/$default_branch" 2>&1 \ + || git -C "$rpath" worktree add -b "$branch" "$worktree" "$default_branch" 2>&1 + )"; then + _cleanup_failed_polecat_dispatch "$SGT_POLECATS/$pname" "$pname" "$rig" "$branch" "$session_name" "$worktree" + _RESLING_LAST_FAILURE_REASON_CODE="worktree-attach-failed" + _RESLING_LAST_FAILURE_DETAIL="${worktree_error:-git worktree add returned non-zero exit status}" + log_event "RESLING_SPAWN_FAILED polecat=$pname rig=$rig issue=#$issue_number branch=$branch source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" reason_code=worktree-attach-failed detail=\"$(_escape_quotes "${_RESLING_LAST_FAILURE_DETAIL}")\"" + echo "[resling] dispatch failed ($source_event) for issue #$issue_number — ${_RESLING_LAST_FAILURE_DETAIL}" >&2 + return 1 + fi _ensure_context_file "$rig" >/dev/null if [[ -f "$rpath/CLAUDE.md" ]]; then @@ -13295,6 +13537,8 @@ _resling_existing_issue() { log_event "RESLING_SKIP_FINAL_GATE issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} skip_reason=\"$(_escape_quotes "$final_gate_reason")\"" git -C "$rpath" worktree remove --force "$worktree" 2>/dev/null || rm -rf "$worktree" git -C "$rpath" branch -D "$branch" 2>/dev/null || true + _RESLING_LAST_FAILURE_REASON_CODE="final-gate" + _RESLING_LAST_FAILURE_DETAIL="$final_gate_reason" return 1 fi @@ -13303,6 +13547,7 @@ RIG=$rig REPO=$repo ISSUE=$issue_number ISSUE_URL=https://github.com/${repo#https://github.com/}/issues/$issue_number +ISSUE_TITLE=$(printf '%q' "$task") BRANCH=$branch WORKTREE=$worktree OUTPUT_LOG=$worktree/.sgt-agent-output.log @@ -13329,6 +13574,8 @@ PSTATE dispatch_reason_code="command-too-long" fi _cleanup_failed_polecat_dispatch "$SGT_POLECATS/$pname" "$pname" "$rig" "$branch" "$session_name" "$worktree" + _RESLING_LAST_FAILURE_REASON_CODE="$dispatch_reason_code" + _RESLING_LAST_FAILURE_DETAIL="${tmux_error:-tmux new-session returned non-zero exit status}" log_event "RESLING_SPAWN_FAILED polecat=$pname rig=$rig issue=#$issue_number branch=$branch source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" reason_code=$dispatch_reason_code detail=\"$(_escape_quotes "${tmux_error:-tmux new-session returned non-zero exit status}")\"" echo "[resling] dispatch failed ($source_event) for issue #$issue_number — ${tmux_error:-tmux new-session returned non-zero exit status}" >&2 return 1 @@ -16866,9 +17113,8 @@ cmd_mayor_rig_status() { ensure_init if [[ -n "$rig" ]]; then ensure_rig "$rig" - _mayor_rig_activity_ensure_state "$rig" local state reason changed_at changed_epoch mode meaningful_at meaningful_epoch meaningful_reason wake_at wake_reason - IFS='|' read -r state reason changed_at changed_epoch mode meaningful_at meaningful_epoch meaningful_reason wake_at wake_reason <<< "$(_mayor_rig_activity_state_read "$rig")" + IFS='|' read -r state reason changed_at changed_epoch mode meaningful_at meaningful_epoch meaningful_reason wake_at wake_reason <<< "$(_mayor_rig_activity_status_fields "$rig")" printf '%s state=%s mode=%s since=%s reason=%s\n' "$rig" "${state:-unknown}" "${mode:-none}" "${changed_at:-unknown}" "${reason:-none}" [[ -n "$meaningful_at" ]] && printf ' last_meaningful=%s reason=%s\n' "$meaningful_at" "${meaningful_reason:-unknown}" [[ -n "$wake_at" ]] && printf ' last_wake=%s reason=%s\n' "$wake_at" "${wake_reason:-unknown}" diff --git a/test_ralph_live_polecat_lane_accounting.sh b/test_ralph_live_polecat_lane_accounting.sh new file mode 100755 index 0000000..89387fb --- /dev/null +++ b/test_ralph_live_polecat_lane_accounting.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# test_ralph_live_polecat_lane_accounting.sh — Ralph active lanes must require a live polecat on the issue. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +SGT_SCRIPT="$REPO_ROOT/sgt" +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +extract_fn() { + local name="$1" + awk -v n="$name" ' + $0 ~ "^" n "\\(\\) \\{" {in_fn=1} + in_fn {print} + in_fn && $0 == "}" {exit} + ' "$SGT_SCRIPT" +} + +eval "$(extract_fn _escape_quotes)" +eval "$(extract_fn _polecat_start_grace_secs)" +eval "$(extract_fn _polecat_counts_as_active)" +eval "$(extract_fn _active_polecat_issue_numbers)" +eval "$(extract_fn _plan_state_path)" +eval "$(extract_fn _plan_file_path)" +eval "$(sed -n '/^_plan_ralph_snapshot()/,/^_president_last_action_snapshot()/p' "$SGT_SCRIPT" | sed '$d')" + +export SGT_ROOT="$TMP_ROOT/root" +export SGT_CONFIG="$SGT_ROOT/.sgt" +export SGT_POLECATS="$SGT_CONFIG/polecats" +export SGT_PLAN_STATE_DIR="$SGT_CONFIG/plan-state" +export SGT_LOG="$TMP_ROOT/sgt.log" +mkdir -p "$SGT_POLECATS" "$SGT_PLAN_STATE_DIR" "$SGT_ROOT/rigs/pmkb" "$TMP_ROOT/worktrees/stale" "$TMP_ROOT/worktrees/live" +: > "$SGT_LOG" + +log_event() { + printf '%s\n' "${1:-}" >> "$SGT_LOG" +} + +rig_path() { + printf '%s/rigs/%s\n' "$SGT_ROOT" "$1" +} + +TMUX_LIVE=1 +tmux() { + if [[ "${1:-}" == "has-session" && "${3:-}" == "sgt-pmkb-live" && "$TMUX_LIVE" == "1" ]]; then + return 0 + fi + return 1 +} + +cat > "$SGT_POLECATS/pmkb-stale" < "$SGT_POLECATS/pmkb-live" < "$SGT_ROOT/rigs/pmkb/SGT_PLAN.json" <<'JSON' +{ + "version": 1, + "rig": "pmkb", + "ralph": { + "enabled": true, + "condition": "Keep PMKB lanes live", + "target_concurrency": 2 + }, + "acceptance": { "status": "pending" }, + "tasks": [ + { "id": "PK1300", "title": "Stale lane" }, + { "id": "PK1301", "title": "Live lane" } + ] +} +JSON + +cat > "$SGT_PLAN_STATE_DIR/pmkb.json" <<'JSON' +{ + "tasks": { + "PK1300": { "status": "in_progress", "issue_number": "1300" }, + "PK1301": { "status": "in_progress", "issue_number": "1301" } + } +} +JSON + +snapshot="$(_plan_ralph_snapshot pmkb)" +IFS='|' read -r enabled unmet condition target active underfilled policy <<< "$snapshot" +[[ "$enabled" == "1" ]] +[[ "$unmet" == "1" ]] +[[ "$target" == "2" ]] +[[ "$active" == "1" ]] || { + echo "expected only the live polecat issue to count active, got snapshot=$snapshot" >&2 + exit 1 +} +[[ "$underfilled" == "1" ]] + +TMUX_LIVE=0 +rm -rf "$TMP_ROOT/worktrees/live" +snapshot="$(_plan_ralph_snapshot pmkb)" +IFS='|' read -r _enabled _unmet _condition _target active underfilled _policy <<< "$snapshot" +[[ "$active" == "0" ]] || { + echo "expected missing live worktree to remove remaining active lane, got snapshot=$snapshot" >&2 + exit 1 +} +[[ "$underfilled" == "1" ]] + +echo "ALL TESTS PASSED" diff --git a/test_ralph_refill_attach_blocker.sh b/test_ralph_refill_attach_blocker.sh index 391a87b..81ea1b2 100644 --- a/test_ralph_refill_attach_blocker.sh +++ b/test_ralph_refill_attach_blocker.sh @@ -42,6 +42,11 @@ _context_append_acceptance_blocker() { :; } _context_python_bin() { return 1; } _context_index_build() { :; } _wake_mayor() { printf '%s\n' "$1" >> "$WAKE_FILE"; } +_workflow_backend_default() { printf '%s\n' 'github'; } +_forge_issue_comment_add() { + local _backend="$1" repo="$2" issue="$3" body="$4" + gh issue comment "$issue" --repo "$repo" --body "$body" +} _acceptance_blocker_write() { printf '%s|%s|%s|%s\n' "$1" "$3" "$4" "blocker-ralph" >> "$BLOCKER_FILE" printf 'blocker-ralph\n' diff --git a/test_resling_authorization_fallback.sh b/test_resling_authorization_fallback.sh new file mode 100755 index 0000000..b3c2013 --- /dev/null +++ b/test_resling_authorization_fallback.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# test_resling_authorization_fallback.sh — Authorization checks should use REST/list fallbacks and classify read failures. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +SGT_SCRIPT="$REPO_ROOT/sgt" + +extract_fn() { + local name="$1" + awk -v n="$name" ' + $0 ~ "^" n "\\(\\) \\{" {in_fn=1} + in_fn {print} + in_fn && $0 == "}" {exit} + ' "$SGT_SCRIPT" +} + +eval "$(extract_fn _one_line)" +eval "$(extract_fn _repo_owner_repo)" +eval "$(extract_fn _gh_issue_labels_live)" +eval "$(extract_fn _labels_contain_sgt_authorized)" +eval "$(extract_fn _label_list_one_line)" +eval "$(extract_fn _has_sgt_authorized)" + +_workflow_backend_default() { printf '%s\n' 'github'; } +_forge_issue_labels() { return 0; } + +gh() { + if [[ "${1:-}" == "api" ]]; then + printf '%s\n' 'sgt-authorized' + printf '%s\n' 'plan' + return 0 + fi + echo "unexpected gh call: $*" >&2 + return 1 +} + +_has_sgt_authorized "https://github.com/acme/demo" "1300" || { + echo "expected REST fallback to authorize issue" >&2 + exit 1 +} +[[ "${_SGT_AUTHZ_LAST_REASON_CODE:-}" == "authorized" ]] +[[ "${_SGT_AUTHZ_LAST_DETAIL:-}" == *"github-rest"* ]] + +gh() { + if [[ "${1:-}" == "api" ]]; then + echo "gh api backend unavailable" >&2 + return 1 + fi + if [[ "${1:-}" == "issue" && "${2:-}" == "list" ]]; then + echo "gh issue list backend unavailable" >&2 + return 1 + fi + echo "unexpected gh call: $*" >&2 + return 1 +} + +if _has_sgt_authorized "https://github.com/acme/demo" "1301"; then + echo "expected read failure to block authorization" >&2 + exit 1 +fi +[[ "${_SGT_AUTHZ_LAST_REASON_CODE:-}" == "label-read-failed" ]] || { + echo "expected label-read-failed, got ${_SGT_AUTHZ_LAST_REASON_CODE:-unset}" >&2 + exit 1 +} +[[ "${_SGT_AUTHZ_LAST_DETAIL:-}" == *"rest_error="* ]] +[[ "${_SGT_AUTHZ_LAST_DETAIL:-}" == *"list_error="* ]] + +echo "ALL TESTS PASSED" diff --git a/test_status_json.sh b/test_status_json.sh index cf1a720..0efebb5 100755 --- a/test_status_json.sh +++ b/test_status_json.sh @@ -43,11 +43,11 @@ args=" $* " pr_state="${SGT_TEST_PR_STATE:-OPEN}" pr_number="${SGT_TEST_PR_NUMBER:-123}" -if [[ "$args" == *" pr list "* ]] && [[ "$args" == *" --json number,state,title "* ]]; then +if [[ "$args" == *" pr list "* ]] && { [[ "$args" == *" --json number,state,title "* ]] || [[ "$args" == *" --json number,state,title,headRefOid "* ]]; }; then if [[ "$pr_state" == "NONE" ]]; then echo "" else - printf '%s\t%s\t%s\n' "$pr_number" "$pr_state" "Mock PR $pr_state" + printf '%s\t%s\t%s\t%s\n' "$pr_number" "$pr_state" "Mock PR $pr_state" "deadbeef" fi exit 0 fi diff --git a/test_status_rig_activity_live_accounting.sh b/test_status_rig_activity_live_accounting.sh new file mode 100755 index 0000000..5d62d20 --- /dev/null +++ b/test_status_rig_activity_live_accounting.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# test_status_rig_activity_live_accounting.sh — Status should not relay stale active_polecats from persisted rig activity state. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +SGT_SCRIPT="$REPO_ROOT/sgt" +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +HOME_DIR="$TMP_ROOT/home" +MOCK_BIN="$TMP_ROOT/mockbin" +mkdir -p "$HOME_DIR/.local/bin" "$MOCK_BIN" +cp "$SGT_SCRIPT" "$HOME_DIR/.local/bin/sgt" +chmod +x "$HOME_DIR/.local/bin/sgt" + +cat > "$MOCK_BIN/tmux" <<'TMUX' +#!/usr/bin/env bash +set -euo pipefail +exit 1 +TMUX +chmod +x "$MOCK_BIN/tmux" + +cat > "$MOCK_BIN/gh" <<'GH' +#!/usr/bin/env bash +set -euo pipefail +args=" $* " +if [[ "$args" == *" pr list "* && "$args" == *" --json number "* && "$args" == *" --jq "* ]]; then + echo "0" + exit 0 +fi +if [[ "$args" == *" issue list "* && "$args" == *" --json number "* && "$args" == *" --jq "* ]]; then + echo "1" + exit 0 +fi +if [[ "$args" == *" pr list "* && "$args" == *" --json number,state,title "* ]]; then + echo "" + exit 0 +fi +exit 0 +GH +chmod +x "$MOCK_BIN/gh" + +ENV_PREFIX=( + env -i + HOME="$HOME_DIR" + PATH="$MOCK_BIN:$HOME_DIR/.local/bin:/usr/local/bin:/usr/bin:/bin" + TERM=dumb + SGT_ROOT="$HOME_DIR/sgt" +) + +"${ENV_PREFIX[@]}" bash --noprofile --norc <<'BASH' +set -euo pipefail + +sgt init >/dev/null +printf 'https://github.com/acme/demo\n' > "$SGT_ROOT/.sgt/rigs/demo" +mkdir -p "$SGT_ROOT/.sgt/mayor-rig-activity" +cat > "$SGT_ROOT/.sgt/mayor-rig-activity/demo.state" <<'STATE' +STATE=active +LAST_REASON=ralph target=3 active_lanes=3 underfilled=0 plan_rollup=ralph-condition-unmet plan_status=pending open_issues=3 open_prs=0 active_polecats=3 merge_queue=0 pending_plan_requests=0 +CHANGED_AT=2026-04-02T05:00:00+02:00 +CHANGED_EPOCH=1775106000 +HIBERNATION_MODE=none +LAST_MEANINGFUL_AT=2026-04-02T05:00:00+02:00 +LAST_MEANINGFUL_EPOCH=1775106000 +LAST_MEANINGFUL_REASON=stale active_polecats=3 +LAST_WAKE_AT= +LAST_WAKE_REASON= +STATE + +sgt status --json > "$SGT_ROOT/status.json" +sgt mayor rig-status demo > "$SGT_ROOT/rig-status.txt" + +python3 - "$SGT_ROOT/status.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +rigs = {entry["rig"]: entry for entry in payload.get("mayor_rigs", [])} +demo = rigs.get("demo") +assert demo is not None, payload +reason = demo.get("reason", "") +assert "active_polecats=0" in reason, demo +assert "active_polecats=3" not in reason, demo +assert payload.get("summary", {}).get("polecat_count") == 0, payload +PY + +head -n 1 "$SGT_ROOT/rig-status.txt" | grep -q 'active_polecats=0' || { + echo "expected mayor rig-status to use live active_polecats=0" >&2 + cat "$SGT_ROOT/rig-status.txt" >&2 + exit 1 +} +if head -n 1 "$SGT_ROOT/rig-status.txt" | grep -q 'active_polecats=3'; then + echo "rig-status relayed stale active_polecats=3" >&2 + cat "$SGT_ROOT/rig-status.txt" >&2 + exit 1 +fi +BASH + +echo "ALL TESTS PASSED" diff --git a/test_sweep_backend_limited_skip.sh b/test_sweep_backend_limited_skip.sh index 33b393e..f248729 100644 --- a/test_sweep_backend_limited_skip.sh +++ b/test_sweep_backend_limited_skip.sh @@ -34,6 +34,9 @@ log_event() { _mayor_rig_manually_hibernated() { return 1; } _repo_owner_repo() { printf '%s\n' "${1#https://github.com/}"; } _escape_quotes() { printf '%s' "$1"; } +_gh_issue_labels_live() { + gh issue view "$2" --repo "$1" --json labels --jq '.labels[].name' +} _resling_find_existing_issue_polecat() { return 1; } _sweep_watchdog_find_open_pr_for_issue() { return 1; } _ai_backend_default() { echo "codex"; } diff --git a/test_witness_alive_stall_classification.sh b/test_witness_alive_stall_classification.sh index 4607669..4c20c6f 100644 --- a/test_witness_alive_stall_classification.sh +++ b/test_witness_alive_stall_classification.sh @@ -27,6 +27,7 @@ eval "$(extract_fn _polecat_created_age_seconds)" eval "$(extract_fn _polecat_session_root_pid)" eval "$(extract_fn _polecat_find_busy_child)" eval "$(extract_fn _polecat_runtime_classify)" +eval "$(extract_fn _polecat_issue_title)" eval "$(extract_fn _witness_pr_meta)" eval "$(extract_fn _witness_loop)" eval "$(extract_fn _mayor_rig_activity_enabled)" @@ -70,6 +71,20 @@ rig_path() { } _escape_quotes() { printf '%s' "$1"; } +_workflow_backend_default() { echo "github"; } +_forge_pr_find_by_head_tsv() { return 0; } +_forge_issue_field() { + local _backend="$1" _repo="$2" issue="$3" field="$4" + if [[ "$field" == "title" ]]; then + gh issue view "$issue" --repo "$_repo" --json title --jq '.title // ""' + return + fi + return 1 +} +_forge_issue_comment_add() { + local _backend="$1" repo="$2" issue="$3" body="$4" + gh issue comment "$issue" --repo "$repo" --body "$body" +} _write_agent_heartbeat() { :; } _wake_refinery() { printf '%s\n' "$1|$2" >> "$WAKE_FILE"; } _ai_backend_default() { echo "codex"; } diff --git a/test_witness_codex_usage_limit_block.sh b/test_witness_codex_usage_limit_block.sh index d468594..4b2f231 100644 --- a/test_witness_codex_usage_limit_block.sh +++ b/test_witness_codex_usage_limit_block.sh @@ -18,6 +18,7 @@ extract_fn() { } eval "$(extract_fn _witness_record_backend_limit_followup)" +eval "$(extract_fn _polecat_issue_title)" eval "$(extract_fn _polecat_output_log_path)" eval "$(extract_fn _witness_dead_polecat_backend_limit)" eval "$(extract_fn _witness_pr_meta)" @@ -97,6 +98,24 @@ _ensure_labels_exist() { printf '%s\n' "$*" >> "$LABEL_FILE" } +_workflow_backend_default() { echo "github"; } +_forge_pr_find_by_head_tsv() { return 0; } +_forge_issue_field() { + local _backend="$1" _repo="$2" issue="$3" field="$4" + if [[ "$field" == "title" ]]; then + gh issue view "$issue" --repo "$_repo" --json title --jq '.title // ""' + return + fi + return 1 +} +_forge_issue_add_labels() { + local _backend="$1" _repo="$2" issue="$3" labels="$4" + printf '%s|%s\n' "$issue" "$labels" >> "$GH_EDIT_FILE" +} +_forge_issue_comment_add() { + local _backend="$1" repo="$2" issue="$3" body="$4" + gh issue comment "$issue" --repo "$repo" --body "$body" +} _write_agent_heartbeat() { :; } _wake_refinery() { :; } _pr_head_sha() { echo "deadbeef"; } diff --git a/test_witness_manual_hibernation_skip.sh b/test_witness_manual_hibernation_skip.sh index 4bf9524..e73dcf4 100644 --- a/test_witness_manual_hibernation_skip.sh +++ b/test_witness_manual_hibernation_skip.sh @@ -18,6 +18,7 @@ extract_fn() { } eval "$(extract_fn _witness_record_stalled_followup)" +eval "$(extract_fn _polecat_issue_title)" eval "$(extract_fn _polecat_output_log_path)" eval "$(extract_fn _witness_dead_polecat_backend_limit)" eval "$(extract_fn _witness_pr_meta)" @@ -99,6 +100,20 @@ _wake_mayor() { printf '%s\n' "$1" >> "$WAKE_FILE" } +_workflow_backend_default() { echo "github"; } +_forge_pr_find_by_head_tsv() { return 0; } +_forge_issue_field() { + local _backend="$1" _repo="$2" issue="$3" field="$4" + if [[ "$field" == "title" ]]; then + gh issue view "$issue" --repo "$_repo" --json title --jq '.title // ""' + return + fi + return 1 +} +_forge_issue_comment_add() { + local _backend="$1" repo="$2" issue="$3" body="$4" + gh issue comment "$issue" --repo "$repo" --body "$body" +} _write_agent_heartbeat() { :; } _wake_refinery() { :; } _pr_head_sha() { echo "deadbeef"; } diff --git a/test_witness_stalled_followup_guard.sh b/test_witness_stalled_followup_guard.sh index 3e9268b..fdaca7f 100755 --- a/test_witness_stalled_followup_guard.sh +++ b/test_witness_stalled_followup_guard.sh @@ -94,6 +94,20 @@ _wake_mayor() { printf '%s\n' "$1" >> "$WAKE_FILE" } +_workflow_backend_default() { echo "github"; } +_forge_pr_find_by_head_tsv() { return 0; } +_forge_issue_field() { + local _backend="$1" _repo="$2" issue="$3" field="$4" + if [[ "$field" == "title" ]]; then + gh issue view "$issue" --repo "$_repo" --json title --jq '.title // ""' + return + fi + return 1 +} +_forge_issue_comment_add() { + local _backend="$1" repo="$2" issue="$3" body="$4" + gh issue comment "$issue" --repo "$repo" --body "$body" +} _write_agent_heartbeat() { :; } _wake_refinery() { :; } _pr_head_sha() { echo "deadbeef"; }