From a5e17a1f8bc40309400d8c05ae50bd6d8e2249ea Mon Sep 17 00:00:00 2001 From: codejeet Date: Wed, 1 Apr 2026 12:38:22 +0200 Subject: [PATCH] Fence superseded duplicate lane redispatch for #380 --- sgt | 142 +++++++++++- test_plan_tick_superseded_open_issue_guard.sh | 203 ++++++++++++++++++ ...ep_watchdog_superseded_plan_issue_guard.sh | 200 +++++++++++++++++ 3 files changed, 537 insertions(+), 8 deletions(-) create mode 100644 test_plan_tick_superseded_open_issue_guard.sh create mode 100644 test_sweep_watchdog_superseded_plan_issue_guard.sh diff --git a/sgt b/sgt index 355af74..894fd75 100755 --- a/sgt +++ b/sgt @@ -6635,8 +6635,10 @@ for task in tasks: # A materially changed task definition should open fresh work instead # of inheriting stale completion bookkeeping from the previous shape. current = {"status": "pending"} + signature_changed = bool(current_signature and current_signature != task_signature) current.setdefault("status", "pending") current["task_signature"] = task_signature + current["task_signature_changed"] = bool(signature_changed) current["reopen_requested"] = bool(reopen_requested) normalized[task_id] = current @@ -6734,23 +6736,24 @@ for task in tasks: backend, str(state_entry.get("task_signature") or ""), "1" if state_entry.get("reopen_requested") else "0", + "1" if state_entry.get("task_signature_changed") else "0", ]) ) PY } _plan_state_update_task() { - local rig="${1:-}" task_id="${2:-}" status="${3:-}" issue_number="${4:-}" issue_url="${5:-}" + local rig="${1:-}" task_id="${2:-}" status="${3:-}" issue_number="${4:-}" issue_url="${5:-}" clear_issue_binding="${6:-0}" local state_file state_file="$(_plan_state_path "$rig")" mkdir -p "$(dirname "$state_file")" - python3 - "$state_file" "$task_id" "$status" "$issue_number" "$issue_url" <<'PY' + python3 - "$state_file" "$task_id" "$status" "$issue_number" "$issue_url" "$clear_issue_binding" <<'PY' import json import os import sys from datetime import datetime, timezone -state_file, task_id, status, issue_number, issue_url = sys.argv[1:6] +state_file, task_id, status, issue_number, issue_url, clear_issue_binding = sys.argv[1:7] doc = {} if os.path.exists(state_file): with open(state_file, "r", encoding="utf-8") as fh: @@ -6758,6 +6761,9 @@ if os.path.exists(state_file): tasks = doc.setdefault("tasks", {}) entry = tasks.setdefault(task_id, {}) entry["status"] = status +if clear_issue_binding in {"1", "true", "yes", "on"}: + entry.pop("issue_number", None) + entry.pop("issue_url", None) if issue_number: entry["issue_number"] = issue_number if issue_url: @@ -6985,6 +6991,92 @@ print(f"{number}|{url}") PY } +_plan_issue_superseded_reason() { + local rig="${1:-}" repo="${2:-}" issue_number="${3:-}" + local plan_file canonical_plan_file issue_json + canonical_plan_file="$(_plan_file_path "$rig")" + [[ -f "$canonical_plan_file" ]] || return 1 + plan_file="$(_plan_effective_file_path "$rig" 2>/dev/null || true)" + [[ -f "$plan_file" ]] || return 1 + issue_json="$(gh issue view "$issue_number" --repo "$repo" --json title,body,labels 2>/dev/null || true)" + [[ -n "$issue_json" ]] || return 1 + python3 - "$plan_file" "$issue_json" <<'PY' +import json +import re +import sys + +plan_file = sys.argv[1] +issue_json = sys.argv[2] + +def one_line(value): + return str(value or "").replace("\t", " ").replace("\n", " ").strip() + +def norm(value): + return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9]+", " ", str(value or "").lower())).strip() + +def issue_task_text(body, fallback): + lines = str(body or "").splitlines() + in_task = False + for raw in lines: + line = raw.strip() + if not in_task: + if line.lower() == "## task": + in_task = True + continue + if not line: + continue + if line.startswith("#") or line == "---": + break + return one_line(line) + return one_line(fallback) + +try: + with open(plan_file, "r", encoding="utf-8") as fh: + plan = json.load(fh) + issue = json.loads(issue_json) +except Exception: + raise SystemExit(1) + +if not isinstance(plan, dict) or not isinstance(issue, dict): + raise SystemExit(1) + +task_id = "" +for label in issue.get("labels") or []: + if not isinstance(label, dict): + continue + name = str(label.get("name") or "").strip() + if name.startswith("plan-") and name != "plan": + task_id = name[5:] + break + +if not task_id: + raise SystemExit(1) + +task = None +for item in plan.get("tasks") or []: + if not isinstance(item, dict): + continue + if str(item.get("id") or "").strip() == task_id: + task = item + break + +if not isinstance(task, dict): + raise SystemExit(1) + +current_title = one_line(task.get("title") or task.get("task") or task_id) +current_task = one_line(task.get("task") or task.get("title") or task_id) +issue_title = one_line(issue.get("title") or "") +issue_task = issue_task_text(issue.get("body") or "", issue_title) + +if norm(issue_task) == norm(current_task) or norm(issue_title) == norm(current_title): + raise SystemExit(1) + +print( + f'{task_id}|issue task "{issue_task}" no longer matches current plan task "{current_task}"' +) +PY +} + _plan_find_latest_merged_task_issue() { local repo="${1:-}" task_id="${2:-}" local issues_json pr_json @@ -7091,8 +7183,8 @@ _plan_tick_run() { "$( [[ "$ralph_underfilled" == "1" ]] && printf 'true' || printf 'false' )" \ "$( [[ "$ralph_completion_blocked" == "1" ]] && printf 'true' || printf 'false' )")" local -a task_ids=() - local -A task_title=() task_text=() task_deps=() task_status=() task_issue=() task_issue_url=() task_labels=() task_backend=() task_signature=() task_reopen_requested=() - while IFS=$'\x1f' read -r record_type c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11; do + local -A task_title=() task_text=() task_deps=() task_status=() task_issue=() task_issue_url=() task_labels=() task_backend=() task_signature=() task_reopen_requested=() task_signature_changed=() + while IFS=$'\x1f' read -r record_type c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12; do [[ -n "$record_type" ]] || continue case "$record_type" in CONFIG) @@ -7117,6 +7209,7 @@ _plan_tick_run() { task_backend["$c1"]="$c9" task_signature["$c1"]="$c10" task_reopen_requested["$c1"]="$c11" + task_signature_changed["$c1"]="$c12" ;; esac done < <(_plan_state_snapshot "$rig" "$plan_file" "$canonical_plan_file") @@ -7138,6 +7231,22 @@ _plan_tick_run() { local -A merged_closing_pr_cache=() merged_task_issue_cache=() for task_id in "${task_ids[@]}"; do issue_number="${task_issue[$task_id]}" + if [[ -n "$issue_number" && "${task_signature_changed[$task_id]:-0}" == "1" ]]; then + previous_issue_number="$issue_number" + issue_state="$(gh issue view "$issue_number" --repo "$repo" --json state --jq '.state // ""' 2>/dev/null || true)" + active_polecat="" + if [[ "$issue_state" == "OPEN" ]]; then + active_polecat="$(_resling_find_existing_issue_polecat "$rig" "$repo" "$issue_number" 2>/dev/null || true)" + fi + if [[ "$issue_state" != "OPEN" || -z "$active_polecat" ]]; then + _plan_state_update_task "$rig" "$task_id" "pending" "" "" "1" + task_status["$task_id"]="pending" + task_issue["$task_id"]="" + task_issue_url["$task_id"]="" + issue_number="" + log_event "PLAN_TASK_CLEAR_STALE_ISSUE_BINDING rig=$rig task=$task_id previous_issue=#${previous_issue_number:-unknown} reason_code=material-task-change issue_state=${issue_state:-unknown} source=$tick_source" + fi + fi if [[ "${task_reopen_requested[$task_id]:-0}" != "1" ]]; then if [[ -v "merged_task_issue_cache[$task_id]" ]]; then merged_task_snapshot="${merged_task_issue_cache[$task_id]}" @@ -7145,7 +7254,7 @@ _plan_tick_run() { merged_task_snapshot="$(_plan_find_latest_merged_task_issue "$repo" "$task_id" 2>/dev/null || true)" merged_task_issue_cache["$task_id"]="$merged_task_snapshot" fi - if [[ -n "$merged_task_snapshot" && "$merged_task_snapshot" == *"|"* ]]; then + if [[ "${task_signature_changed[$task_id]:-0}" != "1" && -n "$merged_task_snapshot" && "$merged_task_snapshot" == *"|"* ]]; then IFS='|' read -r merged_task_issue_number merged_task_issue_url merged_task_pr_number merged_task_pr_merged_at merged_task_issue_state <<< "$merged_task_snapshot" if [[ -n "$merged_task_issue_number" && ( "${task_status[$task_id]}" != "completed" || "$issue_number" != "$merged_task_issue_number" ) ]]; then _plan_state_update_task "$rig" "$task_id" "completed" "$merged_task_issue_number" "${merged_task_issue_url:-${task_issue_url[$task_id]}}" @@ -10200,7 +10309,7 @@ _sweep_watchdog_find_open_pr_for_issue() { _sweep_watchdog_resling_open_authorized_issues() { local rig="${1:-}" repo="${2:-}" - local issue_rows issue_number issue_title existing_polecat open_pr backend_limited_reason + local issue_rows issue_number issue_title existing_polecat open_pr backend_limited_reason superseded_reason local source_event source_event_key source_event="sweep-watchdog" [[ -n "$rig" && -n "$repo" ]] || return 0 @@ -10221,6 +10330,10 @@ _sweep_watchdog_resling_open_authorized_issues() { log_event "SWEEP_WATCHDOG_RESLING_SKIP issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=$backend_limited_reason source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\"" continue fi + if superseded_reason="$(_plan_issue_superseded_reason "$rig" "$repo" "$issue_number" 2>/dev/null || true)" && [[ -n "$superseded_reason" ]]; then + log_event "SWEEP_WATCHDOG_RESLING_SKIP issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=superseded-plan-task-issue source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" detail=\"$(_escape_quotes "$superseded_reason")\"" + continue + fi open_pr="$(_sweep_watchdog_find_open_pr_for_issue "$repo" "$issue_number" || true)" if [[ -n "$open_pr" ]]; then log_event "SWEEP_WATCHDOG_RESLING_SKIP issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=open-pr-exists pr=#$open_pr source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\"" @@ -10238,7 +10351,7 @@ _sweep_watchdog_resling_open_authorized_issues() { _mayor_recover_stranded_actionable_rig() { local rig="${1:-}" repo="${2:-}" local snapshot snapshot_state snapshot_reason open_authorized open_prs active_polecats merge_queue pending_plan_requests plan_rollup plan_status - local issue_rows issue_number issue_title existing_polecat open_pr backend_limited_reason source_event source_event_key + local issue_rows issue_number issue_title existing_polecat open_pr backend_limited_reason superseded_reason source_event source_event_key local resling_output blocked_reason_code blocked_detail dispatched_limit resling_log local pending_plan_target ralph_enabled ralph_condition ralph_condition_status ralph_target _ralph_count_support ralph_state ralph_reason ralph_active_lanes ralph_admissible_lanes ralph_backlog_lanes ralph_support_lanes ralph_duplicate_lanes ralph_underfilled ralph_completion_blocked local recovery_target recovery_due_to_underfill @@ -10317,6 +10430,12 @@ _mayor_recover_stranded_actionable_rig() { log_event "MAYOR_STRANDED_RIG_RECOVERY_BLOCKED issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=$backend_limited_reason source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\"" continue fi + if superseded_reason="$(_plan_issue_superseded_reason "$rig" "$repo" "$issue_number" 2>/dev/null || true)" && [[ -n "$superseded_reason" ]]; then + _MAYOR_STRANDED_RECOVERY_BLOCKED=$((_MAYOR_STRANDED_RECOVERY_BLOCKED + 1)) + _MAYOR_STRANDED_RECOVERY_DETAILS+="${issue_number}|superseded-plan-task-issue|${superseded_reason}"$'\n' + log_event "MAYOR_STRANDED_RIG_RECOVERY_BLOCKED issue=#$issue_number rig=$rig repo=$(_repo_owner_repo "$repo") reason_code=superseded-plan-task-issue source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" detail=\"$(_escape_quotes "$superseded_reason")\"" + continue + fi open_pr="$(_sweep_watchdog_find_open_pr_for_issue "$repo" "$issue_number" || true)" if [[ -n "$open_pr" ]]; then @@ -11158,6 +11277,13 @@ _resling_existing_issue() { return 1 fi + local superseded_reason="" + if superseded_reason="$(_plan_issue_superseded_reason "$rig" "$repo" "$issue_number" 2>/dev/null || true)" && [[ -n "$superseded_reason" ]]; then + echo "[resling] issue #$issue_number is superseded by current repo-local plan task — skipping" + log_event "RESLING_SKIP_SUPERSEDED issue=#$issue_number rig=$rig source_event=$source_event source_event_key=\"$(_escape_quotes "$source_event_key")\" source_pr=${source_pr:-none} reason_code=superseded-plan-task-issue detail=\"$(_escape_quotes "$superseded_reason")\"" + return 1 + fi + local rpath pname branch session_name _RESLING_LAST_POLECAT="" rpath="$(rig_path "$rig")" diff --git a/test_plan_tick_superseded_open_issue_guard.sh b/test_plan_tick_superseded_open_issue_guard.sh new file mode 100644 index 0000000..796f31f --- /dev/null +++ b/test_plan_tick_superseded_open_issue_guard.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Regression: plan tick should clear a stale open plan-task issue when the task changed materially and dispatch the fresh successor instead. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +SGT_SCRIPT="$REPO_ROOT/sgt" +TMP_HOME="$(mktemp -d)" +trap 'rm -rf "$TMP_HOME"' EXIT + +mkdir -p "$TMP_HOME/.local/bin" "$TMP_HOME/mock-bin" "$TMP_HOME/state" +cp "$SGT_SCRIPT" "$TMP_HOME/.local/bin/sgt" +chmod +x "$TMP_HOME/.local/bin/sgt" + +cat > "$TMP_HOME/mock-bin/openclaw" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +EOF +chmod +x "$TMP_HOME/mock-bin/openclaw" + +cat > "$TMP_HOME/mock-bin/tmux" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +case "${1:-}" in + has-session) + exit 1 + ;; + new-session|kill-session) + exit 0 + ;; + *) + exit 0 + ;; +esac +EOF +chmod +x "$TMP_HOME/mock-bin/tmux" + +cat > "$TMP_HOME/mock-bin/git" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "-C" ]]; then + shift 2 +fi +case "${1:-}" in + fetch) + exit 0 + ;; + symbolic-ref) + echo "refs/remotes/origin/main" + exit 0 + ;; + worktree) + shift + case "${1:-}" in + add) + shift + if [[ "${1:-}" == "-b" ]]; then + worktree="${3:-}" + else + worktree="${1:-}" + fi + mkdir -p "$worktree" + exit 0 + ;; + remove) + exit 0 + ;; + esac + ;; + branch) + exit 0 + ;; +esac +exit 0 +EOF +chmod +x "$TMP_HOME/mock-bin/git" + +cat > "$TMP_HOME/mock-bin/gh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +case "${1:-}:${2:-}" in + label:create|issue:edit|pr:view|api:*) + exit 0 + ;; + issue:list) + label="" + state_filter="open" + while [[ $# -gt 0 ]]; do + case "$1" in + --label) label="${2:-}"; shift 2 ;; + --state) state_filter="${2:-}"; shift 2 ;; + --json|--repo|--limit) shift 2 ;; + --jq) shift 2 ;; + *) shift ;; + esac + done + if [[ "$label" == "plan-PKNXT-15" && "$state_filter" == "all" ]]; then + echo '[]' + else + echo '[]' + fi + ;; + issue:view) + issue_number="${3:-}" + issue_number="${issue_number#\#}" + shift 3 + json_fields="" + while [[ $# -gt 0 ]]; do + case "$1" in + --json) json_fields="${2:-}"; shift 2 ;; + --jq) shift 2 ;; + --repo) shift 2 ;; + *) shift ;; + esac + done + case "$issue_number:$json_fields" in + 1095:state) echo "OPEN" ;; + *) echo "" ;; + esac + ;; + pr:list) + echo '[]' + ;; + issue:create) + echo "https://github.com/acme/demo/issues/1097" + ;; + *) + exit 0 + ;; +esac +EOF +chmod +x "$TMP_HOME/mock-bin/gh" + +COMMON_ENV=( + "HOME=$TMP_HOME" + "PATH=$TMP_HOME/mock-bin:$TMP_HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" + "TERM=${TERM:-xterm}" + "SGT_MAYOR_DISPATCH_COOLDOWN=0" +) + +env -i "${COMMON_ENV[@]}" bash --noprofile --norc <<'BASH' +set -euo pipefail + +sgt init >/dev/null +mkdir -p "$HOME/sgt/.sgt/rigs" "$HOME/sgt/rigs/demo" "$HOME/sgt/.sgt/plan-state" +printf '%s\n' 'https://github.com/acme/demo' > "$HOME/sgt/.sgt/rigs/demo" +cat > "$HOME/sgt/rigs/demo/SGT_PLAN.json" <<'JSON' +{ + "version": 1, + "rig": "demo", + "policy": { "max_in_flight": 1 }, + "tasks": [ + { "id": "PKNXT-15", "title": "Implement and evaluate late_expiry_taker_h5_h10_passive_improvement_proof_subset_lane as the primary widened-corpus continuation lane", "status": "pending" } + ] +} +JSON +cat > "$HOME/sgt/.sgt/plan-state/demo.json" <<'JSON' +{ + "tasks": { + "PKNXT-15": { + "issue_number": "1095", + "issue_url": "https://github.com/acme/demo/issues/1095", + "status": "dispatched", + "task_signature": "old-confidence-lift-signature", + "updated_at": "2026-04-01T09:47:00Z" + } + } +} +JSON + +sgt plan tick demo > "$HOME/plan-tick.out" 2>&1 +BASH + +STATE_FILE="$TMP_HOME/sgt/.sgt/plan-state/demo.json" + +python3 - "$STATE_FILE" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +task = data["tasks"]["PKNXT-15"] +assert task["status"] == "dispatched", task +assert str(task.get("issue_number")) == "1097", task +assert task.get("task_signature_changed") is False, task +PY + +grep -q 'PLAN_TASK_CLEAR_STALE_ISSUE_BINDING rig=demo task=PKNXT-15 previous_issue=#1095 reason_code=material-task-change issue_state=OPEN source=manual' "$TMP_HOME/sgt/sgt.log" || { + echo "expected stale open issue binding to be cleared" >&2 + cat "$TMP_HOME/sgt/sgt.log" >&2 + exit 1 +} + +grep -q 'dispatched_now=1' "$TMP_HOME/plan-tick.out" || { + echo "expected fresh successor dispatch after stale open issue clear" >&2 + cat "$TMP_HOME/plan-tick.out" >&2 + exit 1 +} + +echo "ALL TESTS PASSED" diff --git a/test_sweep_watchdog_superseded_plan_issue_guard.sh b/test_sweep_watchdog_superseded_plan_issue_guard.sh new file mode 100644 index 0000000..9149681 --- /dev/null +++ b/test_sweep_watchdog_superseded_plan_issue_guard.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Regression: sweep watchdog should skip open authorized plan issues that no longer match the current repo-local task definition. + +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" +TMUX_NEW_SESSION_COUNT="$TMP_ROOT/tmux-new-session-count" +mkdir -p "$HOME_DIR/.local/bin" "$MOCK_BIN" +cp "$SGT_SCRIPT" "$HOME_DIR/.local/bin/sgt" +chmod +x "$HOME_DIR/.local/bin/sgt" +printf '0\n' > "$TMUX_NEW_SESSION_COUNT" + +cat > "$MOCK_BIN/gh" <<'GH' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == "issue" && "${2:-}" == "list" ]]; then + shift 2 + json_fields="" + label="" + state_filter="open" + while [[ $# -gt 0 ]]; do + case "$1" in + --json) + json_fields="${2:-}" + shift 2 + ;; + --label) + label="${2:-}" + shift 2 + ;; + --state) + state_filter="${2:-}" + shift 2 + ;; + --jq) + shift 2 + ;; + --repo|--limit) + shift 2 + ;; + *) + shift + ;; + esac + done + if [[ "$json_fields" == "number,title" ]]; then + printf '77\tImplement and evaluate late_expiry_taker_h5_only_reset_confirmation_confidence_lift_gate as the immediate next continuation lane\n' + elif [[ "$label" == "plan-PKNXT-15" && "$state_filter" == "all" ]]; then + echo '[]' + else + echo '[]' + fi + exit 0 +fi + +if [[ "${1:-}" == "issue" && "${2:-}" == "view" ]]; then + issue_number="${3:-}" + issue_number="${issue_number#\#}" + shift 3 + json_fields="" + while [[ $# -gt 0 ]]; do + case "$1" in + --json) + json_fields="${2:-}" + shift 2 + ;; + --jq) + shift 2 + ;; + --repo) + shift 2 + ;; + *) + shift + ;; + esac + done + case "$json_fields" in + labels) echo "sgt-authorized plan plan-PKNXT-15" ;; + state) echo "OPEN" ;; + title) echo "Implement and evaluate late_expiry_taker_h5_only_reset_confirmation_confidence_lift_gate as the immediate next continuation lane" ;; + body) cat <<'EOF' +## Task + +Implement and evaluate late_expiry_taker_h5_only_reset_confirmation_confidence_lift_gate as the immediate next continuation lane +EOF + ;; + title,body,labels) cat <<'EOF' +{"title":"Implement and evaluate late_expiry_taker_h5_only_reset_confirmation_confidence_lift_gate as the immediate next continuation lane","body":"## Task\n\nImplement and evaluate late_expiry_taker_h5_only_reset_confirmation_confidence_lift_gate as the immediate next continuation lane\n","labels":[{"name":"sgt-authorized"},{"name":"plan"},{"name":"plan-PKNXT-15"}]} +EOF + ;; + *) echo "" ;; + esac + exit 0 +fi + +if [[ "${1:-}" == "pr" && "${2:-}" == "list" ]]; then + exit 0 +fi + +if [[ "${1:-}" == "label" && "${2:-}" == "create" ]]; then + exit 0 +fi + +echo "mock gh unsupported: $*" >&2 +exit 1 +GH +chmod +x "$MOCK_BIN/gh" + +cat > "$MOCK_BIN/tmux" <<'TMUX' +#!/usr/bin/env bash +set -euo pipefail + +COUNT_FILE="${SGT_MOCK_TMUX_NEW_SESSION_COUNT:?missing SGT_MOCK_TMUX_NEW_SESSION_COUNT}" + +if [[ "${1:-}" == "new-session" ]]; then + n="$(cat "$COUNT_FILE")" + n=$((n + 1)) + printf '%s\n' "$n" > "$COUNT_FILE" + exit 0 +fi + +if [[ "${1:-}" == "has-session" ]]; then + exit 1 +fi + +exit 0 +TMUX +chmod +x "$MOCK_BIN/tmux" + +cat > "$MOCK_BIN/git" <<'GIT' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "-C" ]]; then + shift 2 +fi +case "${1:-}" in + fetch) + exit 0 + ;; + symbolic-ref) + echo "refs/remotes/origin/master" + exit 0 + ;; + worktree|branch) + exit 0 + ;; +esac +echo "mock git unsupported: $*" >&2 +exit 1 +GIT +chmod +x "$MOCK_BIN/git" + +env -i \ + HOME="$HOME_DIR" \ + PATH="$MOCK_BIN:$HOME_DIR/.local/bin:/usr/local/bin:/usr/bin:/bin" \ + TERM="${TERM:-xterm}" \ + SGT_ROOT="$HOME_DIR/sgt" \ + SGT_MOCK_TMUX_NEW_SESSION_COUNT="$TMUX_NEW_SESSION_COUNT" \ + bash --noprofile --norc -c ' +set -euo pipefail + +sgt init >/dev/null +mkdir -p "$SGT_ROOT/rigs/test" +printf "https://github.com/acme/demo\n" > "$SGT_ROOT/.sgt/rigs/test" +cat > "$SGT_ROOT/rigs/test/SGT_PLAN.json" <<'"'"'JSON'"'"' +{ + "version": 1, + "rig": "test", + "policy": { "max_in_flight": 1 }, + "tasks": [ + { "id": "PKNXT-15", "title": "Implement and evaluate late_expiry_taker_h5_h10_passive_improvement_proof_subset_lane as the primary widened-corpus continuation lane", "status": "pending" } + ] +} +JSON + +sgt sweep > "$SGT_ROOT/sweep.out" 2>&1 +' + +if [[ "$(cat "$TMUX_NEW_SESSION_COUNT")" != "0" ]]; then + echo "expected sweep watchdog to skip superseded plan issue" >&2 + exit 1 +fi + +LOG_FILE="$HOME_DIR/sgt/sgt.log" + +grep -q 'SWEEP_WATCHDOG_RESLING_SKIP issue=#77 rig=test repo=acme/demo reason_code=superseded-plan-task-issue' "$LOG_FILE" || { + echo "expected structured superseded-plan watchdog skip telemetry" >&2 + cat "$LOG_FILE" >&2 + exit 1 +} + +echo "ALL TESTS PASSED"