diff --git a/.github/scripts/local_verify_pypi_slice.py b/.github/scripts/local_verify_pypi_slice.py index 11897d2f0..00267e678 100644 --- a/.github/scripts/local_verify_pypi_slice.py +++ b/.github/scripts/local_verify_pypi_slice.py @@ -24,7 +24,7 @@ REPO_ROOT / "docs" / "solutions" / "testing" / "verify-pypi-regression-closeout.md" ) PLAN_020 = REPO_ROOT / "docs" / "plans" / "2026-05-24-020-verify-pypi-regression-post-268-plan.md" -PLAN_TRACK_CAP = "114" +PLAN_TRACK_CAP = "214" LFG_EXIT_CODES: dict[int, str] = { 0: "proceed, merge_ready, or monitoring_complete", 1: "gh_error", @@ -36,6 +36,42 @@ "from pr_ci_recommendation" ), } +_LFG_RUN_REF_FIELDS = ( + "verify_run_id", + "fc_run_id", + "verify_run_url", + "fc_run_url", + "verify_status", + "fc_status", +) +LFG_FLAT_FIELD_KEYS: tuple[str, ...] = ( + "active_runs", + "gh_watch_summary", + "queue_context", + "queue_backlog", + "queue_backlog_severe", + "queue_backlog_warning", + "max_queued_hours", + "queue_backlog_note", + "expected_after_terminal", + "primary_action", + "watch_recommended", + "post_terminal_commands", + "wait_command", + "briefing_command", + "monitor_commands", + *_LFG_RUN_REF_FIELDS, + "blocked", + "briefing_action", + "briefing_reason", + "briefing_notes", + "briefing_merge_ready", + "sha_gap", + "sha_gap_short", + "gh_watch_command", + "wait_recommended", + "ci_drift", +) _AUTO_APPLY_PROCEED_REASONS = frozenset({"update_monitoring_docs", "investigate_ci_drift"}) _DISPATCH_PROCEED_REASONS = frozenset({"refresh_verify_dispatch", "refresh_fc_dispatch"}) VERIFY_WORKFLOW = "verify-pypi-regression.yml" @@ -74,6 +110,7 @@ ) _ACTIVE_STATUSES = frozenset({"queued", "in_progress", "pending", "waiting", "requested"}) _QUEUE_BACKLOG_HOURS = 4.0 +_QUEUE_WARN_HOURS = 2.0 CORE_CHECK = """ import pykotor @@ -493,8 +530,7 @@ def _compare_checkpoint(status: dict[str, Any]) -> dict[str, Any]: queue_suffix = "" if isinstance(queued_hours, (int, float)): queue_suffix = f"; queued {queued_hours:.1f}h" - result.update( - { + pending_update: dict[str, Any] = { "checkpoint_unchanged": True, "defer_lfg_pr": True, "defer_reason": "FC run still active; classify SHA gap after terminal", @@ -503,7 +539,11 @@ def _compare_checkpoint(status: dict[str, Any]) -> dict[str, Any]: f"vs master {master_sha[:7] if master_sha else '?'}{queue_suffix}" ), } - ) + if isinstance(queued_hours, (int, float)) and queued_hours >= _QUEUE_BACKLOG_HOURS: + pending_update["queue_backlog_note"] = ( + f"FC queued {queued_hours:.1f}h (external runner backlog)" + ) + result.update(pending_update) return result if fc_sha_stale and fc_sha_stale_benign is None: @@ -1644,23 +1684,360 @@ def _is_lfg_checkpoint_deferred(status: dict[str, Any]) -> bool: return isinstance(checkpoint, dict) and bool(checkpoint.get("defer_lfg_pr")) -def _format_preflight_watch_poll_line(polls: int, status: dict[str, Any]) -> str: +def _extract_gh_watch_command(briefing: dict[str, Any]) -> str | None: + monitor_commands = briefing.get("monitor_commands") + if not isinstance(monitor_commands, dict): + return None + watch_cmd = monitor_commands.get("watch_fc_run") or monitor_commands.get( + "watch_verify_run" + ) + if isinstance(watch_cmd, str) and watch_cmd: + return watch_cmd + return None + + +def _primary_watch_command(commands: dict[str, str]) -> str: + gate_watch = commands.get("gate_watch") + if isinstance(gate_watch, str) and gate_watch: + return gate_watch + preflight_watch = commands.get("preflight_watch") + if isinstance(preflight_watch, str) and preflight_watch: + return preflight_watch + return "" + + +def _watch_label_display(watch_label: str) -> str: + normalized = watch_label.strip().lower() + if normalized == "gate": + return "gate watch" + return "preflight watch" + + +def _lfg_briefing_fallback(status: dict[str, Any]) -> dict[str, Any]: + briefing = status.get("lfg_agent_briefing") + if isinstance(briefing, dict): + return briefing + if isinstance(status.get("action"), str): + return status + return {} + + +def _lfg_briefing_mirror_stderr_parts(status: dict[str, Any]) -> list[str]: + parts: list[str] = [] + briefing = _lfg_briefing_fallback(status) + + primary_action = status.get("primary_action") + if not isinstance(primary_action, str) or not primary_action: + primary_action = briefing.get("primary_action") + if isinstance(primary_action, str) and primary_action: + parts.append(f"primary_action={primary_action}") + + expected_after = status.get("expected_after_terminal") + if not isinstance(expected_after, dict): + expected_after = briefing.get("expected_after_terminal") + if isinstance(expected_after, dict): + after_action = expected_after.get("action") + if isinstance(after_action, str) and after_action: + parts.append(f"expected_after={after_action}") + + active_runs = status.get("active_runs") + if not isinstance(active_runs, list) or not active_runs: + active_runs = briefing.get("active_runs") + if isinstance(active_runs, list) and active_runs: + parts.append(f"active_runs={','.join(str(label) for label in active_runs)}") + + watch_recommended = status.get("watch_recommended") + if not watch_recommended: + watch_recommended = briefing.get("watch_recommended") + if watch_recommended: + parts.append("watch_recommended=true") + + gh_watch_command = status.get("gh_watch_command") + if not isinstance(gh_watch_command, str) or not gh_watch_command: + gh_watch_command = _extract_gh_watch_command(briefing) + if isinstance(gh_watch_command, str) and gh_watch_command: + parts.append(f"watch={gh_watch_command}") + + command = status.get("briefing_command") or status.get("wait_command") + if not isinstance(command, str) or not command: + command = briefing.get("command") + if isinstance(command, str) and command: + parts.append(f"briefing_command={_format_briefing_command_stderr(command)}") + + sha_gap_short = status.get("sha_gap_short") + if not isinstance(sha_gap_short, str) or not sha_gap_short: + sha_gap_short = _format_briefing_sha_gap_short(briefing) + if isinstance(sha_gap_short, str) and sha_gap_short: + parts.append(f"sha_gap={sha_gap_short}") + + queue_note = status.get("queue_backlog_note") + if not isinstance(queue_note, str) or not queue_note: + queue_context = briefing.get("queue_context") + if isinstance(queue_context, dict): + nested_note = queue_context.get("note") + if isinstance(nested_note, str) and nested_note: + queue_note = nested_note + if isinstance(queue_note, str) and queue_note: + parts.append(f"queue_note={_format_queue_backlog_note_stderr(queue_note)}") + + blocked = status.get("blocked") + if not isinstance(blocked, str) or not blocked: + blocked = briefing.get("blocked") + if isinstance(blocked, str) and blocked: + parts.append(f"blocked={blocked}") + + briefing_reason = status.get("briefing_reason") + if not isinstance(briefing_reason, str) or not briefing_reason: + briefing_reason = briefing.get("reason") + if isinstance(briefing_reason, str) and briefing_reason: + parts.append(f"briefing_reason={briefing_reason}") + + briefing_action = status.get("briefing_action") + if not isinstance(briefing_action, str) or not briefing_action: + briefing_action = briefing.get("action") + if isinstance(briefing_action, str) and briefing_action: + parts.append(f"action={briefing_action}") + + notes = status.get("briefing_notes") + if not isinstance(notes, list) or not notes: + notes = briefing.get("notes") + if isinstance(notes, list) and notes: + parts.append(f"notes={len(notes)}") + + if "briefing_merge_ready" in status: + parts.append( + f"merge_ready={'true' if status['briefing_merge_ready'] else 'false'}" + ) + else: + merge_ready = _format_briefing_merge_ready(briefing) + if merge_ready is not None: + parts.append(f"merge_ready={merge_ready}") + + verify_run_id = status.get("verify_run_id") + if verify_run_id is None: + verify_run_id = briefing.get("verify_run_id") + if verify_run_id is not None: + parts.append(f"verify_run={verify_run_id}") + + fc_run_id = status.get("fc_run_id") + if fc_run_id is None: + fc_run_id = briefing.get("fc_run_id") + if fc_run_id is not None: + parts.append(f"fc_run={fc_run_id}") + + verify_run_url = status.get("verify_run_url") + if not isinstance(verify_run_url, str) or not verify_run_url: + verify_run_url = briefing.get("verify_run_url") + if isinstance(verify_run_url, str) and verify_run_url: + parts.append(f"verify_run_url={_format_run_url_stderr(verify_run_url)}") + + fc_run_url = status.get("fc_run_url") + if not isinstance(fc_run_url, str) or not fc_run_url: + fc_run_url = briefing.get("fc_run_url") + if isinstance(fc_run_url, str) and fc_run_url: + parts.append(f"fc_run_url={_format_run_url_stderr(fc_run_url)}") + + verify_status = status.get("verify_status") + if not isinstance(verify_status, str) or not verify_status: + verify_status = briefing.get("verify_status") + if isinstance(verify_status, str) and verify_status: + parts.append(f"verify_status={verify_status}") + + fc_status = status.get("fc_status") + if not isinstance(fc_status, str) or not fc_status: + fc_status = briefing.get("fc_status") + if isinstance(fc_status, str) and fc_status: + parts.append(f"fc_status={fc_status}") + + gh_watch_summary = status.get("gh_watch_summary") + if not isinstance(gh_watch_summary, str) or not gh_watch_summary: + gh_watch_summary = briefing.get("gh_watch_summary") + if not isinstance(gh_watch_summary, str) or not gh_watch_summary: + gh_watch_summary = _format_gh_watch_summary(briefing) + if isinstance(gh_watch_summary, str) and gh_watch_summary: + parts.append(f"gh_watch={gh_watch_summary}") + + max_queued = status.get("max_queued_hours") + queue_backlog_severe = status.get("queue_backlog_severe") + queue_backlog = status.get("queue_backlog") + queue_backlog_warning = status.get("queue_backlog_warning") + if not isinstance(max_queued, (int, float)): + own_queue_context = status.get("queue_context") + if isinstance(own_queue_context, dict): + nested_queued = own_queue_context.get("max_queued_hours") + if isinstance(nested_queued, (int, float)): + max_queued = nested_queued + if not queue_backlog_severe and not queue_backlog: + queue_backlog_severe = own_queue_context.get("queue_backlog_severe") + queue_backlog = own_queue_context.get("queue_backlog") + queue_backlog_warning = own_queue_context.get("queue_backlog_warning") + if not isinstance(max_queued, (int, float)): + queue_context = briefing.get("queue_context") + if isinstance(queue_context, dict): + nested_queued = queue_context.get("max_queued_hours") + if isinstance(nested_queued, (int, float)): + max_queued = nested_queued + if not queue_backlog_severe and not queue_backlog: + queue_backlog_severe = queue_context.get("queue_backlog_severe") + queue_backlog = queue_context.get("queue_backlog") + queue_backlog_warning = queue_context.get("queue_backlog_warning") + if isinstance(max_queued, (int, float)): + parts.append(f"queued={float(max_queued):.1f}h") + if queue_backlog_severe or queue_backlog: + parts.append("queue_backlog=true") + elif queue_backlog_warning: + parts.append("queue_warn=true") + + action = status.get("briefing_action") + if not isinstance(action, str) or not action: + action = status.get("action") + if not isinstance(action, str) or not action: + action = briefing.get("action") + wait_recommended = status.get("wait_recommended") + if wait_recommended is None: + wait_recommended = briefing.get("wait_recommended") + if action == "investigate_ci_drift" and wait_recommended: + parts.append("wait=true") + drift_fields = _lfg_briefing_drift_field_names(status) + if drift_fields: + parts.append(f"drift_fields={','.join(drift_fields)}") + + parts.extend(_lfg_flat_field_mirror_stderr_parts(status)) + + return parts + + +def _format_preflight_watch_poll_line( + polls: int, + status: dict[str, Any], + *, + watch_label: str = "preflight", + previous_flat_keys: list[str] | None = None, + flat_keys_unchanged_streak: int = 0, + flat_keys_heartbeat_polls: int = 12, + flat_keys_heartbeat_count: int | None = None, +) -> str: reason = status.get("lfg_defer_reason") or "deferred" - parts = [f"LFG preflight watch poll {polls}: deferred=true reason={reason}"] + label = _watch_label_display(watch_label) + parts = [f"LFG {label} poll {polls}: deferred=true reason={reason}"] + emit_briefing_status = bool(status.get("lfg_deferred")) + checkpoint = status.get("checkpoint") + if isinstance(checkpoint, dict): + master_sha = checkpoint.get("master_sha") + forward_commits = status.get("forward_commits") + fc_head = ( + forward_commits.get("head_sha") + if isinstance(forward_commits, dict) + else None + ) + if ( + isinstance(master_sha, str) + and isinstance(fc_head, str) + and not emit_briefing_status + ): + parts.append(f"sha_gap={fc_head[:7]}:{master_sha[:7]}") for key, label in (("forward_commits", "fc"), ("verify_pypi", "verify")): run = status.get(key) if not isinstance(run, dict) or "error" in run: continue run_id = run.get("run_id") - if run_id is not None: + if run_id is not None and not emit_briefing_status: parts.append(f"{label}={run_id}") - parts.append(f"{label}_status={_run_display_label(run)}") + if not emit_briefing_status: + parts.append(f"{label}_status={_run_display_label(run)}") queued = run.get("queued_hours") - if isinstance(queued, (int, float)): + if isinstance(queued, (int, float)) and not emit_briefing_status: parts.append(f"{label}_queued={queued:.1f}h") + if not emit_briefing_status: + active_runs = _build_active_runs_list(status) + if active_runs: + parts.append(f"active_runs={','.join(active_runs)}") + gh_watch = _build_gh_watch_from_status(status) + if gh_watch: + parts.append(f"gh_watch={gh_watch}") + queue_context = _build_defer_queue_context(status) + max_queued = queue_context.get("max_queued_hours") + if isinstance(max_queued, (int, float)): + parts.append(f"queued={float(max_queued):.1f}h") + if queue_context.get("queue_backlog_severe"): + parts.append("queue_backlog=true") + elif queue_context.get("queue_backlog_warning"): + parts.append("queue_warn=true") + if status.get("lfg_deferred"): + _apply_lfg_agent_briefing(status) + mirror_parts = _lfg_briefing_mirror_stderr_parts(status) + current_flat_keys = _lfg_flat_field_keys_present_stderr(status) + flat_keys_unchanged = ( + previous_flat_keys is not None + and current_flat_keys + and previous_flat_keys == current_flat_keys + ) + parts.extend( + _preflight_watch_poll_flat_stderr_parts( + mirror_parts, + flat_keys_unchanged=flat_keys_unchanged, + flat_keys_unchanged_streak=flat_keys_unchanged_streak, + flat_keys_heartbeat_polls=flat_keys_heartbeat_polls, + flat_keys_heartbeat_count=flat_keys_heartbeat_count, + ) + ) return " ".join(parts) +def _count_unchanged_preflight_flat_keys_polls(history: list[dict[str, Any]]) -> int: + if len(history) < 2: + return 0 + count = 0 + for index in range(1, len(history)): + prev_keys = history[index - 1].get("flat_keys") + curr_keys = history[index].get("flat_keys") + if ( + isinstance(prev_keys, list) + and isinstance(curr_keys, list) + and prev_keys + and prev_keys == curr_keys + ): + count += 1 + return count + + +def _max_preflight_flat_unchanged_streak(history: list[dict[str, Any]]) -> int: + max_streak = 0 + for entry in history: + streak = entry.get("flat_unchanged") + if isinstance(streak, int) and streak > max_streak: + max_streak = streak + return max_streak + + +def _max_preflight_flat_hb_total(history: list[dict[str, Any]]) -> int: + max_hb = 0 + for entry in history: + hb = entry.get("flat_hb_total") + if not isinstance(hb, int) or hb <= 0: + hb = entry.get("flat_hb") + if isinstance(hb, int) and hb > max_hb: + max_hb = hb + return max_hb + + +def _resolve_preflight_flat_keys_heartbeats( + status: dict[str, Any], + history: list[dict[str, Any]], +) -> int: + heartbeats = int(status.get("preflight_flat_keys_heartbeats") or 0) + if heartbeats > 0: + return heartbeats + return _max_preflight_flat_hb_total(history) + + +def _resolve_preflight_unchanged_flat_keys_polls(history: list[dict[str, Any]]) -> int: + unchanged = _count_unchanged_preflight_flat_keys_polls(history) + if unchanged > 0: + return unchanged + return _max_preflight_flat_unchanged_streak(history) + + def _build_preflight_watch_summary(status: dict[str, Any]) -> dict[str, Any]: history = list(status.get("preflight_watch_history") or []) started = status.get("preflight_watch_started_monotonic") @@ -1672,21 +2049,202 @@ def _build_preflight_watch_summary(status: dict[str, Any]) -> dict[str, Any]: if history: first_reason = history[0].get("lfg_defer_reason") last_reason = history[-1].get("lfg_defer_reason") - return { + watch_heartbeat_polls = int(status.get("preflight_watch_heartbeat_polls") or 0) + flat_keys_heartbeats = _resolve_preflight_flat_keys_heartbeats(status, history) + unchanged_flat_keys_polls = _resolve_preflight_unchanged_flat_keys_polls(history) + max_flat_unchanged = _max_preflight_flat_unchanged_streak(history) + summary: dict[str, Any] = { "polls": len(history), "lfg_preflight_watch_result": status.get("lfg_preflight_watch_result"), "start_defer_reason": first_reason, "end_defer_reason": last_reason, "watch_duration_sec": duration_sec, + "unchanged_flat_keys_polls": unchanged_flat_keys_polls, + "flat_keys_heartbeat_polls": flat_keys_heartbeats, + "watch_heartbeat_polls": watch_heartbeat_polls, } + if watch_heartbeat_polls > 0: + summary["heartbeat_every"] = watch_heartbeat_polls + if flat_keys_heartbeats > 0: + summary["flat_hb"] = flat_keys_heartbeats + summary["flat_hb_total"] = flat_keys_heartbeats + if isinstance(unchanged_flat_keys_polls, int) and unchanged_flat_keys_polls > 0: + summary["flat_unchanged"] = unchanged_flat_keys_polls + if max_flat_unchanged > 0: + summary["max_flat_unchanged"] = max_flat_unchanged + return summary + + +def _preflight_unchanged_flat_keys_polls(summary: dict[str, Any]) -> int: + flat_unchanged = summary.get("flat_unchanged") + if isinstance(flat_unchanged, int) and flat_unchanged > 0: + return flat_unchanged + unchanged = summary.get("unchanged_flat_keys_polls") + if isinstance(unchanged, int) and unchanged > 0: + return unchanged + return 0 + + +def _preflight_flat_keys_heartbeat_count(summary: dict[str, Any]) -> int: + flat_hb_total = summary.get("flat_hb_total") + if isinstance(flat_hb_total, int) and flat_hb_total > 0: + return flat_hb_total + flat_hb = summary.get("flat_hb") + if isinstance(flat_hb, int) and flat_hb > 0: + return flat_hb + heartbeats = summary.get("flat_keys_heartbeat_polls") + if isinstance(heartbeats, int) and heartbeats > 0: + return heartbeats + return 0 + + +def _preflight_watch_heartbeat_interval(summary: dict[str, Any]) -> int: + heartbeat_every = summary.get("heartbeat_every") + if isinstance(heartbeat_every, int) and heartbeat_every > 0: + return heartbeat_every + watch_heartbeat = summary.get("watch_heartbeat_polls") + if isinstance(watch_heartbeat, int) and watch_heartbeat > 0: + return watch_heartbeat + return 0 + + +def _preflight_max_flat_unchanged(summary: dict[str, Any]) -> int: + max_flat = summary.get("max_flat_unchanged") + if isinstance(max_flat, int) and max_flat > 0: + return max_flat + return 0 + + +def _preflight_max_flat_unchanged_for_stderr(summary: dict[str, Any]) -> int: + unchanged = _preflight_unchanged_flat_keys_polls(summary) + max_flat = _preflight_max_flat_unchanged(summary) + if unchanged > 0 and max_flat > 0 and max_flat < unchanged: + return max_flat + return 0 + + +def _should_emit_preflight_flat_keys_heartbeat_summary(summary: dict[str, Any]) -> bool: + heartbeats = _preflight_flat_keys_heartbeat_count(summary) + if heartbeats <= 0: + return False + unchanged = _preflight_unchanged_flat_keys_polls(summary) + if unchanged <= 0: + return False + interval = _preflight_watch_heartbeat_interval(summary) + if interval <= 0: + return True + return unchanged >= interval -def _format_preflight_watch_summary_line(summary: dict[str, Any]) -> str: +def _preflight_flat_hb_total_for_stderr(summary: dict[str, Any]) -> int: + if not _should_emit_preflight_flat_keys_heartbeat_summary(summary): + return 0 + return _preflight_flat_keys_heartbeat_count(summary) + + +def _preflight_heartbeat_every_for_stderr(summary: dict[str, Any]) -> int: + if _preflight_unchanged_flat_keys_polls(summary) <= 0: + return 0 + return _preflight_watch_heartbeat_interval(summary) + + +def _preflight_watch_poll_flat_stderr_parts( + mirror_parts: list[str], + *, + flat_keys_unchanged: bool, + flat_keys_unchanged_streak: int, + flat_keys_heartbeat_polls: int, + flat_keys_heartbeat_count: int | None = None, +) -> list[str]: + emit_flat_keys_heartbeat = _should_emit_watch_heartbeat( + flat_keys_unchanged, + flat_keys_unchanged_streak, + flat_keys_heartbeat_polls, + ) + if flat_keys_unchanged and not emit_flat_keys_heartbeat: + parts = [ + part + for part in mirror_parts + if not part.startswith("flat_keys=") + and not part.startswith("flat_fields=") + ] + parts.append( + f"flat_unchanged={flat_keys_unchanged_streak if flat_keys_unchanged_streak > 0 else 1}" + ) + elif flat_keys_unchanged and emit_flat_keys_heartbeat: + parts = list(mirror_parts) + heartbeat_count = ( + flat_keys_heartbeat_count + if isinstance(flat_keys_heartbeat_count, int) and flat_keys_heartbeat_count > 0 + else 1 + ) + parts.append(f"flat_hb={heartbeat_count}") + else: + parts = list(mirror_parts) + if flat_keys_unchanged and flat_keys_heartbeat_polls > 0: + parts.append(f"heartbeat_every={flat_keys_heartbeat_polls}") + return parts + + +def _preflight_watch_summary_unchanged_flat_stderr_parts( + summary: dict[str, Any], +) -> list[str]: + parts: list[str] = [] + unchanged_flat = _preflight_unchanged_flat_keys_polls(summary) + if unchanged_flat: + parts.append(f"flat_unchanged={unchanged_flat}") + max_flat_unchanged = _preflight_max_flat_unchanged_for_stderr(summary) + if max_flat_unchanged > 0: + parts.append(f"max_flat_unchanged={max_flat_unchanged}") + heartbeat_interval = _preflight_heartbeat_every_for_stderr(summary) + if heartbeat_interval > 0: + parts.append(f"heartbeat_every={heartbeat_interval}") + return parts + + +def _preflight_watch_summary_heartbeat_flat_stderr_parts( + summary: dict[str, Any], +) -> list[str]: + parts: list[str] = [] + flat_hb_total = _preflight_flat_hb_total_for_stderr(summary) + if flat_hb_total > 0: + parts.append(f"flat_hb_total={flat_hb_total}") + return parts + + +def _preflight_watch_summary_flat_stderr_parts(summary: dict[str, Any]) -> list[str]: + parts = _preflight_watch_summary_unchanged_flat_stderr_parts(summary) + parts.extend(_preflight_watch_summary_heartbeat_flat_stderr_parts(summary)) + return parts + + +def _format_preflight_watch_summary_line( + summary: dict[str, Any], + *, + watch_label: str = "preflight", +) -> str: result = summary.get("lfg_preflight_watch_result") or "unknown" polls = summary.get("polls", 0) duration = summary.get("watch_duration_sec") duration_text = f"{duration:.0f}s" if isinstance(duration, (int, float)) else "n/a" - return f"result={result} polls={polls} duration={duration_text}" + parts = [f"result={result} polls={polls} duration={duration_text}"] + parts.extend(_preflight_watch_summary_flat_stderr_parts(summary)) + start_reason = summary.get("start_defer_reason") + end_reason = summary.get("end_defer_reason") + if ( + isinstance(start_reason, str) + and isinstance(end_reason, str) + and start_reason + and end_reason + and start_reason != end_reason + ): + parts.append(f"reason={start_reason}->{end_reason}") + next_hint = summary.get("next_hint") + if isinstance(next_hint, str) and next_hint: + hint = next_hint if len(next_hint) <= 96 else f"{next_hint[:93]}..." + parts.append(f"next={hint}") + parts.extend(_lfg_briefing_mirror_stderr_parts(summary)) + return " ".join(parts) def _watch_lfg_preflight_defer( @@ -1695,6 +2253,8 @@ def _watch_lfg_preflight_defer( prefetch_git: bool, interval_sec: float, timeout_sec: float, + watch_label: str = "preflight", + flat_keys_heartbeat_polls: int = 12, ) -> dict[str, Any]: deadline = time.monotonic() + max(0.0, timeout_sec) polls = 0 @@ -1702,6 +2262,10 @@ def _watch_lfg_preflight_defer( status: dict[str, Any] = {} status["preflight_watch_started_monotonic"] = time.monotonic() watch_result = "proceed" + previous_flat_keys: list[str] | None = None + flat_keys_unchanged_streak = 0 + status["preflight_flat_keys_heartbeats"] = 0 + status["preflight_watch_heartbeat_polls"] = max(0, flat_keys_heartbeat_polls) while True: polls += 1 prefetch_result = None @@ -1736,8 +2300,53 @@ def _watch_lfg_preflight_defer( queued = run.get("queued_hours") if isinstance(queued, (int, float)): snapshot[f"{prefix}_queued_hours"] = round(float(queued), 2) + current_flat_keys: list[str] = [] + if status.get("lfg_deferred"): + _apply_lfg_agent_briefing(status) + current_flat_keys = _lfg_flat_field_keys_present_stderr(status) + if ( + previous_flat_keys is not None + and current_flat_keys + and previous_flat_keys == current_flat_keys + ): + flat_keys_unchanged_streak += 1 + else: + flat_keys_unchanged_streak = 0 + flat_keys_unchanged = ( + previous_flat_keys is not None + and current_flat_keys + and previous_flat_keys == current_flat_keys + ) + emit_flat_keys_heartbeat = _should_emit_watch_heartbeat( + flat_keys_unchanged, + flat_keys_unchanged_streak, + flat_keys_heartbeat_polls, + ) + heartbeat_count = int(status.get("preflight_flat_keys_heartbeats") or 0) + if emit_flat_keys_heartbeat: + heartbeat_count += 1 + print( + _format_preflight_watch_poll_line( + polls, + status, + watch_label=watch_label, + previous_flat_keys=previous_flat_keys, + flat_keys_unchanged_streak=flat_keys_unchanged_streak, + flat_keys_heartbeat_polls=flat_keys_heartbeat_polls, + flat_keys_heartbeat_count=heartbeat_count if emit_flat_keys_heartbeat else None, + ), + file=sys.stderr, + ) + if current_flat_keys: + snapshot["flat_keys"] = list(current_flat_keys) + if flat_keys_unchanged_streak > 0: + snapshot["flat_unchanged"] = flat_keys_unchanged_streak + if emit_flat_keys_heartbeat: + status["preflight_flat_keys_heartbeats"] = heartbeat_count + snapshot["flat_hb"] = heartbeat_count + snapshot["flat_hb_total"] = heartbeat_count + previous_flat_keys = current_flat_keys history.append(snapshot) - print(_format_preflight_watch_poll_line(polls, status), file=sys.stderr) if not still_deferred: watch_result = "proceed" break @@ -1749,11 +2358,40 @@ def _watch_lfg_preflight_defer( status["preflight_watch_polls"] = polls status["lfg_preflight_watch_result"] = watch_result summary = _build_preflight_watch_summary(status) + blocked = _lfg_refresh_blocked(status, deferred=bool(status.get("lfg_deferred"))) + summary["next_hint"] = _build_proceed_hint(status, blocked=blocked) + if status.get("lfg_deferred"): + _apply_lfg_agent_briefing(status) + _mirror_preflight_watch_summary_from_status(status, summary) + else: + active_runs = _build_active_runs_list(status) + if active_runs: + summary["active_runs"] = active_runs + gh_watch = _build_gh_watch_from_status(status) + if gh_watch: + summary["gh_watch_summary"] = gh_watch + queue_context = _build_defer_queue_context(status) + if queue_context.get("max_queued_hours") is not None or queue_context.get( + "queue_backlog" + ): + summary["queue_context"] = queue_context + _mirror_queue_context_fields( + summary, + summary.get("queue_context") if isinstance(summary.get("queue_context"), dict) else None, + ) + _mirror_queue_backlog_note( + summary, + summary.get("queue_context") if isinstance(summary.get("queue_context"), dict) else None, + ) status["preflight_watch_summary"] = summary + label = _watch_label_display(watch_label) print( - f"Preflight watch summary: {_format_preflight_watch_summary_line(summary)}", + f"LFG {label} summary: {_format_preflight_watch_summary_line(summary, watch_label=watch_label)}", file=sys.stderr, ) + next_hint = summary.get("next_hint") + if isinstance(next_hint, str) and next_hint: + print(f"LFG {label} next: {next_hint}", file=sys.stderr) return status @@ -2092,6 +2730,15 @@ def _compute_lfg_exit_reason( return "unknown" +def _should_attach_lfg_mirror_stderr(status: dict[str, Any]) -> bool: + if isinstance(status.get("lfg_agent_briefing"), dict): + return True + flat_values = status.get("lfg_flat_field_values") + if isinstance(flat_values, dict) and flat_values: + return True + return _lfg_flat_field_stderr_count(status) > 0 + + def _emit_lfg_strict_exit_stderr(status: dict[str, Any], exit_code: int) -> None: reason = status.get("lfg_exit_reason") if reason is None: @@ -2104,6 +2751,10 @@ def _emit_lfg_strict_exit_stderr(status: dict[str, Any], exit_code: int) -> None crosscheck_note = status.get("pr_checks_crosscheck_note") if crosscheck_note: line = f"{line} crosscheck={crosscheck_note}" + if _should_attach_lfg_mirror_stderr(status): + suffix = " ".join(_lfg_briefing_mirror_stderr_parts(status)) + if suffix: + line = f"{line} {suffix}" print(line, file=sys.stderr) @@ -2121,17 +2772,231 @@ def _attach_active_run_refs(status: dict[str, Any], briefing: dict[str, Any]) -> briefing[f"{prefix}_status"] = _run_display_label(run) -def _build_defer_monitor_commands(briefing: dict[str, Any]) -> dict[str, str]: +def _build_active_runs_list(status: dict[str, Any]) -> list[str]: + active_runs: list[str] = [] + for key, label in (("verify_pypi", "verify"), ("forward_commits", "fc")): + run = status.get(key) + if isinstance(run, dict) and "error" not in run and _is_active_run(run): + active_runs.append(label) + return active_runs + + +def _build_gh_watch_from_status(status: dict[str, Any]) -> str: + parts: list[str] = [] + for key, label in (("verify_pypi", "verify"), ("forward_commits", "fc")): + run = status.get(key) + if not isinstance(run, dict) or "error" in run or not _is_active_run(run): + continue + run_id = run.get("run_id") + if run_id is not None: + parts.append(f"{label}:{run_id}") + return ",".join(parts) + + +def _build_ci_drift_detail(status: dict[str, Any]) -> dict[str, Any]: + checkpoint = status.get("checkpoint") if isinstance(status.get("checkpoint"), dict) else {} + doc_validation = ( + status.get("doc_validation") if isinstance(status.get("doc_validation"), dict) else {} + ) + active_runs = _build_active_runs_list(status) + return { + "fields": list(doc_validation.get("drift") or []), + "status_drift": list(doc_validation.get("status_drift") or []), + "ci_drift_note": checkpoint.get("ci_drift_note"), + "active_runs": active_runs, + "wait_recommended": bool(active_runs), + } + + +def _build_drift_refresh_commands(status: dict[str, Any]) -> dict[str, str]: script = "python3 .github/scripts/local_verify_pypi_slice.py" - command = briefing.get("command") - preflight_retry = ( - str(command) - if isinstance(command, str) and command - else f"{script} --lfg-preflight --json" + commands: dict[str, str] = { + "refresh_dry_run": f"{script} --lfg-refresh --dry-run", + "preflight_retry": f"{script} --lfg-preflight --json", + "preflight_watch": f"{script} --lfg-preflight-watch --json", + "gate_watch": f"{script} --lfg-gate-watch --json", + } + verify = status.get("verify_pypi") + forward_commits = status.get("forward_commits") + verify_terminal = isinstance(verify, dict) and "error" not in verify and not _is_active_run(verify) + fc_terminal = ( + isinstance(forward_commits, dict) + and "error" not in forward_commits + and not _is_active_run(forward_commits) ) + if verify_terminal and fc_terminal: + commands["closeout"] = f"{script} --lfg-closeout" + return commands + + +def _build_drift_expected_after( + refresh_commands: dict[str, str], +) -> dict[str, str] | None: + for key in ("closeout", "refresh_dry_run", "gate", "preflight"): + command = refresh_commands.get(key) + if isinstance(command, str) and command: + return {"action": key, "command": command} + return None + + +def _defer_preflight_watch_recommended(status: dict[str, Any]) -> bool: + defer_reason = status.get("lfg_defer_reason") + if not isinstance(defer_reason, str) or not defer_reason: + defer_reason = _resolve_lfg_defer_reason(status.get("checkpoint")) + return defer_reason in { + "fc_active_pending", + "fc_active_closeout", + "verify_active_closeout", + "unchanged_active_runs", + } + + +def _build_defer_sha_gap_detail(status: dict[str, Any]) -> dict[str, Any] | None: + checkpoint = status.get("checkpoint") + if not isinstance(checkpoint, dict): + return None + if not checkpoint.get("fc_sha_stale") and not checkpoint.get("fc_stale_gap_pending_note"): + return None + forward_commits = status.get("forward_commits") + fc = forward_commits if isinstance(forward_commits, dict) else {} + master_sha = checkpoint.get("master_sha") + fc_head_sha = fc.get("head_sha") + detail: dict[str, Any] = { + "fc_sha_stale": bool(checkpoint.get("fc_sha_stale")), + "master_sha": master_sha, + "fc_head_sha": fc_head_sha, + } + queued_hours = fc.get("queued_hours") + if isinstance(queued_hours, (int, float)): + detail["queued_hours"] = round(float(queued_hours), 2) + if isinstance(master_sha, str) and isinstance(fc_head_sha, str): + detail["short"] = f"{fc_head_sha[:7]}:{master_sha[:7]}" + return detail + + +def _build_defer_queue_context(status: dict[str, Any]) -> dict[str, Any]: + checkpoint = status.get("checkpoint") if isinstance(status.get("checkpoint"), dict) else {} + max_queued: float | None = None + for key in ("forward_commits", "verify_pypi"): + run = status.get(key) + if not isinstance(run, dict) or "error" in run: + continue + queued_hours = run.get("queued_hours") + if isinstance(queued_hours, (int, float)): + value = float(queued_hours) + if max_queued is None or value > max_queued: + max_queued = value + queue_backlog = max_queued is not None and max_queued >= _QUEUE_BACKLOG_HOURS + queue_backlog_warning = ( + max_queued is not None + and max_queued >= _QUEUE_WARN_HOURS + and not queue_backlog + ) + note = checkpoint.get("queue_backlog_note") + context: dict[str, Any] = { + "queue_backlog": queue_backlog, + "queue_backlog_severe": queue_backlog, + "queue_backlog_warning": queue_backlog_warning, + } + if max_queued is not None: + context["max_queued_hours"] = round(max_queued, 2) + if isinstance(note, str) and note: + context["note"] = note + return context + + +def _mirror_queue_backlog_note( + target: dict[str, Any], + queue_context: dict[str, Any] | None, +) -> None: + if isinstance(queue_context, dict): + note = queue_context.get("note") + if isinstance(note, str) and note: + target["queue_backlog_note"] = note + return + target.pop("queue_backlog_note", None) + + +def _format_briefing_command_stderr(command: str) -> str: + if len(command) <= 96: + return command + return f"{command[:93]}..." + + +def _format_queue_backlog_note_stderr(note: str) -> str: + if len(note) <= 96: + return note + return f"{note[:93]}..." + + +def _format_run_url_stderr(url: str) -> str: + if len(url) <= 96: + return url + return f"{url[:93]}..." + + +def _mirror_queue_context_fields( + target: dict[str, Any], + queue_context: dict[str, Any] | None, +) -> None: + keys = ( + "queue_backlog", + "queue_backlog_severe", + "queue_backlog_warning", + "max_queued_hours", + ) + if not isinstance(queue_context, dict) or not queue_context: + for key in keys: + target.pop(key, None) + return + for key in keys: + if key in queue_context: + target[key] = queue_context[key] + else: + target.pop(key, None) + + +def _build_defer_post_terminal_commands(status: dict[str, Any]) -> dict[str, str]: + script = "python3 .github/scripts/local_verify_pypi_slice.py" + commands: dict[str, str] = { + "preflight": f"{script} --lfg-preflight --json", + "gate": f"{script} --lfg-gate", + } + checkpoint = status.get("checkpoint") + if isinstance(checkpoint, dict) and checkpoint.get("fc_sha_stale"): + commands["prefetch_gate"] = ( + f"{script} --prefetch-git --lfg-gate # after FC terminal; classify SHA gap" + ) + defer_reason = status.get("lfg_defer_reason") + if not isinstance(defer_reason, str) or not defer_reason: + defer_reason = _resolve_lfg_defer_reason( + status.get("checkpoint") if isinstance(status.get("checkpoint"), dict) else None + ) + if defer_reason in { + "unchanged_active_runs", + "fc_active_closeout", + "verify_active_closeout", + }: + commands["closeout"] = f"{script} --lfg-closeout" + return commands + + +def _build_defer_expected_after_terminal( + post_terminal_commands: dict[str, str], +) -> dict[str, str] | None: + for key in ("prefetch_gate", "closeout", "gate", "preflight"): + command = post_terminal_commands.get(key) + if isinstance(command, str) and command: + return {"action": key, "command": command} + return None + + +def _build_defer_monitor_commands(briefing: dict[str, Any]) -> dict[str, str]: + script = "python3 .github/scripts/local_verify_pypi_slice.py" commands: dict[str, str] = { - "preflight_retry": preflight_retry, + "preflight_retry": f"{script} --lfg-preflight --json", "preflight_watch": f"{script} --lfg-preflight-watch --json", + "gate_watch": f"{script} --lfg-gate-watch --json", } fc_run_id = briefing.get("fc_run_id") if fc_run_id is not None: @@ -2142,6 +3007,23 @@ def _build_defer_monitor_commands(briefing: dict[str, Any]) -> dict[str, str]: return commands +def _format_gh_watch_summary(briefing: dict[str, Any]) -> str: + monitor_commands = briefing.get("monitor_commands") + if not isinstance(monitor_commands, dict): + return "" + parts: list[str] = [] + for label, cmd_key, id_key in ( + ("verify", "watch_verify_run", "verify_run_id"), + ("fc", "watch_fc_run", "fc_run_id"), + ): + if cmd_key not in monitor_commands: + continue + run_id = briefing.get(id_key) + if run_id is not None: + parts.append(f"{label}:{run_id}") + return ",".join(parts) + + def _build_lfg_agent_briefing(status: dict[str, Any]) -> dict[str, Any]: proceed_hint = str(status.get("proceed_hint") or "") script = "python3 .github/scripts/local_verify_pypi_slice.py" @@ -2220,6 +3102,7 @@ def _build_lfg_agent_briefing(status: dict[str, Any]) -> dict[str, Any]: "fc_stale_gap_pending_note", "fc_active_closeout_note", "verify_active_closeout_note", + "queue_backlog_note", ): note = checkpoint.get(key) if isinstance(note, str) and note: @@ -2235,6 +3118,22 @@ def _build_lfg_agent_briefing(status: dict[str, Any]) -> dict[str, Any]: } _attach_active_run_refs(status, briefing) briefing["monitor_commands"] = _build_defer_monitor_commands(briefing) + briefing["post_terminal_commands"] = _build_defer_post_terminal_commands(status) + expected_after = _build_defer_expected_after_terminal(briefing["post_terminal_commands"]) + if expected_after is not None: + briefing["expected_after_terminal"] = expected_after + sha_gap = _build_defer_sha_gap_detail(status) + if sha_gap is not None: + briefing["sha_gap"] = sha_gap + queue_context = _build_defer_queue_context(status) + briefing["queue_context"] = queue_context + active_runs = _build_active_runs_list(status) + if active_runs: + briefing["active_runs"] = active_runs + if _defer_preflight_watch_recommended(status): + briefing["watch_recommended"] = True + briefing["primary_action"] = "gate_watch" + briefing["command"] = _primary_watch_command(briefing["monitor_commands"]) return briefing blocked_refresh = status.get("lfg_refresh_blocked") if blocked_refresh: @@ -2248,44 +3147,390 @@ def _build_lfg_agent_briefing(status: dict[str, Any]) -> dict[str, Any]: } proceed_reason = checkpoint.get("proceed_reason") if isinstance(checkpoint, dict) else None if proceed_reason == "investigate_ci_drift": - return { + drift = _build_ci_drift_detail(status) + refresh_commands = _build_drift_refresh_commands(status) + command = proceed_hint + if drift.get("wait_recommended"): + command = _primary_watch_command(refresh_commands) + briefing = { "action": "investigate_ci_drift", - "command": proceed_hint, + "command": command, "reason": "investigate_ci_drift", "notes": extra_notes, "merge_ready": False, "blocked": None, + "drift": drift, + "refresh_commands": refresh_commands, + "wait_recommended": bool(drift.get("wait_recommended")), } + expected_after = _build_drift_expected_after(refresh_commands) + if expected_after is not None: + briefing["expected_after_terminal"] = expected_after + _attach_active_run_refs(status, briefing) + if drift.get("wait_recommended"): + briefing["monitor_commands"] = _build_defer_monitor_commands(briefing) + briefing["primary_action"] = "gate_watch" + briefing["queue_context"] = _build_defer_queue_context(status) + active_runs = _build_active_runs_list(status) + if active_runs: + briefing["active_runs"] = active_runs + return briefing return {} +def _mirror_briefing_sha_gap( + target: dict[str, Any], + briefing: dict[str, Any], +) -> None: + sha_gap = briefing.get("sha_gap") + if isinstance(sha_gap, dict) and sha_gap: + target["sha_gap"] = sha_gap + short = sha_gap.get("short") + if isinstance(short, str) and short: + target["sha_gap_short"] = short + else: + target.pop("sha_gap_short", None) + else: + target.pop("sha_gap", None) + target.pop("sha_gap_short", None) + + +def _format_briefing_sha_gap_short(briefing: dict[str, Any]) -> str | None: + sha_gap = briefing.get("sha_gap") + if isinstance(sha_gap, dict): + short = sha_gap.get("short") + if isinstance(short, str) and short: + return short + return None + + +def _mirror_briefing_merge_ready( + target: dict[str, Any], + briefing: dict[str, Any], +) -> None: + if "merge_ready" in briefing: + target["briefing_merge_ready"] = bool(briefing["merge_ready"]) + else: + target.pop("briefing_merge_ready", None) + + +def _format_briefing_merge_ready(briefing: dict[str, Any]) -> str | None: + if "merge_ready" not in briefing: + return None + return "true" if briefing["merge_ready"] else "false" + + +def _mirror_briefing_notes( + target: dict[str, Any], + briefing: dict[str, Any], +) -> None: + notes = briefing.get("notes") + if isinstance(notes, list) and notes: + target["briefing_notes"] = list(notes) + else: + target.pop("briefing_notes", None) + + +def _mirror_lfg_flat_fields( + source: dict[str, Any], + target: dict[str, Any], + *, + clear_missing: bool = False, + queue_context_filter: bool = False, +) -> None: + active_runs = source.get("active_runs") + if isinstance(active_runs, list) and active_runs: + target["active_runs"] = list(active_runs) + elif clear_missing: + target.pop("active_runs", None) + + gh_watch = source.get("gh_watch_summary") + if isinstance(gh_watch, str) and gh_watch: + target["gh_watch_summary"] = gh_watch + elif clear_missing: + target.pop("gh_watch_summary", None) + + queue_context = source.get("queue_context") + queue_context_present = isinstance(queue_context, dict) and queue_context + if queue_context_present: + if not queue_context_filter or ( + queue_context.get("max_queued_hours") is not None + or queue_context.get("queue_backlog") + ): + target["queue_context"] = queue_context + elif clear_missing: + target.pop("queue_context", None) + queue_context = None + elif clear_missing: + target.pop("queue_context", None) + _mirror_queue_context_fields( + target, + queue_context if isinstance(queue_context, dict) else None, + ) + _mirror_queue_backlog_note( + target, + queue_context if isinstance(queue_context, dict) else None, + ) + + expected_after = source.get("expected_after_terminal") + if isinstance(expected_after, dict) and expected_after: + target["expected_after_terminal"] = expected_after + elif clear_missing: + target.pop("expected_after_terminal", None) + + primary_action = source.get("primary_action") + if isinstance(primary_action, str) and primary_action: + target["primary_action"] = primary_action + elif clear_missing: + target.pop("primary_action", None) + + watch_recommended = source.get("watch_recommended") + if watch_recommended: + target["watch_recommended"] = True + elif clear_missing: + target.pop("watch_recommended", None) + + post_terminal = source.get("post_terminal_commands") + if isinstance(post_terminal, dict) and post_terminal: + target["post_terminal_commands"] = post_terminal + elif clear_missing: + target.pop("post_terminal_commands", None) + + command = source.get("briefing_command") or source.get("wait_command") or source.get("command") + if isinstance(command, str) and command: + target["wait_command"] = command + target["briefing_command"] = command + elif clear_missing: + target.pop("wait_command", None) + target.pop("briefing_command", None) + + monitor_commands = source.get("monitor_commands") + if isinstance(monitor_commands, dict) and monitor_commands: + target["monitor_commands"] = monitor_commands + elif clear_missing: + target.pop("monitor_commands", None) + + for field in _LFG_RUN_REF_FIELDS: + value = source.get(field) + if value is not None: + target[field] = value + elif clear_missing: + target.pop(field, None) + + blocked = source.get("blocked") + if isinstance(blocked, str) and blocked: + target["blocked"] = blocked + elif clear_missing: + target.pop("blocked", None) + + action = source.get("briefing_action") + if not isinstance(action, str) or not action: + action = source.get("action") + if isinstance(action, str) and action: + target["briefing_action"] = action + elif clear_missing: + target.pop("briefing_action", None) + + reason = source.get("briefing_reason") + if not isinstance(reason, str) or not reason: + reason = source.get("reason") + if isinstance(reason, str) and reason: + target["briefing_reason"] = reason + elif clear_missing: + target.pop("briefing_reason", None) + + if clear_missing: + _mirror_briefing_notes(target, source) + _mirror_briefing_merge_ready(target, source) + _mirror_briefing_sha_gap(target, source) + else: + notes = source.get("briefing_notes") + if not isinstance(notes, list) or not notes: + notes = source.get("notes") + if isinstance(notes, list) and notes: + target["briefing_notes"] = list(notes) + if "briefing_merge_ready" in source: + target["briefing_merge_ready"] = source["briefing_merge_ready"] + elif "merge_ready" in source: + target["briefing_merge_ready"] = bool(source["merge_ready"]) + sha_gap = source.get("sha_gap") + if isinstance(sha_gap, dict) and sha_gap: + target["sha_gap"] = sha_gap + short = sha_gap.get("short") + if isinstance(short, str) and short: + target["sha_gap_short"] = short + sha_gap_short = source.get("sha_gap_short") + if isinstance(sha_gap_short, str) and sha_gap_short: + target["sha_gap_short"] = sha_gap_short + + gh_watch_command = source.get("gh_watch_command") + if not isinstance(gh_watch_command, str) or not gh_watch_command: + gh_watch_command = _extract_gh_watch_command(source) + if isinstance(gh_watch_command, str) and gh_watch_command: + target["gh_watch_command"] = gh_watch_command + elif clear_missing: + target.pop("gh_watch_command", None) + + wait_recommended = source.get("wait_recommended") + if wait_recommended: + target["wait_recommended"] = True + elif clear_missing: + target.pop("wait_recommended", None) + + ci_drift = source.get("ci_drift") + if not isinstance(ci_drift, dict) or not ci_drift: + ci_drift = source.get("drift") + if isinstance(ci_drift, dict) and ci_drift: + target["ci_drift"] = ci_drift + elif clear_missing: + target.pop("ci_drift", None) + + +def _format_briefing_notes_count(briefing: dict[str, Any]) -> str | None: + notes = briefing.get("notes") + if isinstance(notes, list) and notes: + return str(len(notes)) + return None + + +def _attach_gh_watch_summary(briefing: dict[str, Any]) -> None: + gh_watch = _format_gh_watch_summary(briefing) + if gh_watch: + briefing["gh_watch_summary"] = gh_watch + + +def _build_lfg_flat_field_values(source: dict[str, Any]) -> dict[str, Any]: + values: dict[str, Any] = {} + for key in LFG_FLAT_FIELD_KEYS: + if key not in source: + continue + value = source[key] + if value is None: + continue + if isinstance(value, bool): + values[key] = value + continue + if isinstance(value, str) and not value: + continue + if isinstance(value, (list, dict)) and not value: + continue + values[key] = value + return values + + +def _build_lfg_flat_field_keys_present(flat_values: dict[str, Any]) -> list[str]: + return [key for key in LFG_FLAT_FIELD_KEYS if key in flat_values] + + +def _mirror_preflight_watch_summary_from_status( + status: dict[str, Any], + summary: dict[str, Any], +) -> None: + _mirror_lfg_flat_fields( + status, + summary, + clear_missing=False, + queue_context_filter=True, + ) + flat_keys = status.get("lfg_flat_field_keys") + if isinstance(flat_keys, list) and flat_keys: + summary["lfg_flat_field_keys"] = list(flat_keys) + flat_values = _build_lfg_flat_field_values(summary) + if flat_values: + summary["lfg_flat_field_values"] = flat_values + summary["lfg_flat_field_keys_present"] = _build_lfg_flat_field_keys_present( + flat_values + ) + + +def _lfg_flat_field_keys_present_stderr(source: dict[str, Any]) -> list[str]: + present = source.get("lfg_flat_field_keys_present") + if isinstance(present, list) and present: + return [str(key) for key in present if isinstance(key, str) and key] + flat_values = source.get("lfg_flat_field_values") + if isinstance(flat_values, dict) and flat_values: + return _build_lfg_flat_field_keys_present(flat_values) + return _build_lfg_flat_field_keys_present(_build_lfg_flat_field_values(source)) + + +def _lfg_flat_field_stderr_count(source: dict[str, Any]) -> int: + flat_values = source.get("lfg_flat_field_values") + if isinstance(flat_values, dict): + return len(flat_values) + return len(_build_lfg_flat_field_values(source)) + + +def _lfg_flat_field_mirror_stderr_parts(source: dict[str, Any]) -> list[str]: + parts: list[str] = [] + flat_count = _lfg_flat_field_stderr_count(source) + if flat_count: + parts.append(f"flat_fields={flat_count}") + flat_keys = _lfg_flat_field_keys_present_stderr(source) + if flat_keys: + parts.append(f"flat_keys={','.join(flat_keys)}") + return parts + + def _apply_lfg_agent_briefing(status: dict[str, Any]) -> None: briefing = _build_lfg_agent_briefing(status) if briefing: + _attach_gh_watch_summary(briefing) status["lfg_agent_briefing"] = briefing + _mirror_lfg_flat_fields(briefing, status, clear_missing=True) + status["lfg_flat_field_keys"] = list(LFG_FLAT_FIELD_KEYS) + flat_values = _build_lfg_flat_field_values(status) + if flat_values: + status["lfg_flat_field_values"] = flat_values + status["lfg_flat_field_keys_present"] = _build_lfg_flat_field_keys_present( + flat_values + ) + else: + status.pop("lfg_flat_field_values", None) + status.pop("lfg_flat_field_keys_present", None) else: status.pop("lfg_agent_briefing", None) + status.pop("lfg_flat_field_keys", None) + status.pop("lfg_flat_field_values", None) + status.pop("lfg_flat_field_keys_present", None) + _mirror_lfg_flat_fields({}, status, clear_missing=True) + + +def _lfg_briefing_drift_field_names(status: dict[str, Any]) -> list[str]: + ci_drift = status.get("ci_drift") + if isinstance(ci_drift, dict): + drift = ci_drift + else: + briefing = _lfg_briefing_fallback(status) + drift = briefing.get("drift") + if not isinstance(drift, dict): + return [] + fields = drift.get("fields") or [] + return [ + str(entry.get("field")) + for entry in fields + if isinstance(entry, dict) and entry.get("field") + ] -def _emit_lfg_agent_briefing_stderr(briefing: dict[str, Any]) -> None: - action = briefing.get("action") or "unknown" +def _emit_lfg_agent_briefing_stderr(status: dict[str, Any]) -> None: + briefing = _lfg_briefing_fallback(status) + action = status.get("briefing_action") or briefing.get("action") or "unknown" parts = [f"action={action}"] - if action == "defer" and briefing.get("reason"): - parts.append(f"reason={briefing['reason']}") + if action == "defer": + reason = ( + status.get("briefing_reason") + or status.get("lfg_defer_reason") + or briefing.get("reason") + ) + if isinstance(reason, str) and reason: + parts.append(f"reason={reason}") + skip_prefixes = {"action=", "briefing_reason="} + for part in _lfg_briefing_mirror_stderr_parts(status): + if any(part.startswith(prefix) for prefix in skip_prefixes): + continue + parts.append(part) if "exit_code" in briefing: parts.append(f"exit={briefing['exit_code']}") - if briefing.get("blocked"): - parts.append(f"blocked={briefing['blocked']}") - fc_run_id = briefing.get("fc_run_id") - if fc_run_id is not None: - parts.append(f"fc_run={fc_run_id}") - monitor_commands = briefing.get("monitor_commands") - if isinstance(monitor_commands, dict): - watch_cmd = monitor_commands.get("watch_fc_run") or monitor_commands.get( - "watch_verify_run" - ) - if isinstance(watch_cmd, str) and watch_cmd: - parts.append(f"watch={watch_cmd}") percent = briefing.get("completion_percent") if isinstance(percent, int): parts.append(f"complete={percent}%") @@ -2717,10 +3962,16 @@ def _build_proceed_hint(status: dict[str, Any], *, blocked: str | None) -> str: script = "python3 .github/scripts/local_verify_pypi_slice.py" if blocked == "deferred": defer_reason = _resolve_lfg_defer_reason(status.get("checkpoint")) - if defer_reason in {"fc_active_pending", "fc_active_closeout"}: - return f"{script} --lfg-preflight # re-check when FC run reaches terminal" - if defer_reason == "verify_active_closeout": - return f"{script} --lfg-preflight # re-check when verify run reaches terminal" + if defer_reason in { + "fc_active_pending", + "fc_active_closeout", + "verify_active_closeout", + "unchanged_active_runs", + }: + return ( + f"{script} --lfg-gate-watch --json " + "# poll until active runs reach terminal" + ) return f"{script} --lfg-gate" if blocked == "classify_fc_stale_gap": return f"{script} --prefetch-git --lfg-gate" @@ -2738,6 +3989,12 @@ def _build_proceed_hint(status: dict[str, Any], *, blocked: str | None) -> str: if proceed_reason == "monitoring_complete": return f"{script} --lfg-gate # monitoring docs synced; track complete" if proceed_reason == "investigate_ci_drift": + drift = _build_ci_drift_detail(status) + if drift.get("wait_recommended"): + return ( + f"{script} --lfg-gate-watch --json " + "# wait for active runs before refresh dry-run" + ) return f"{script} --lfg-refresh --dry-run" if proceed_reason in _DISPATCH_PROCEED_REASONS: return f"{script} --lfg-refresh" @@ -2752,6 +4009,7 @@ def _resolve_lfg_mode( lfg_merge_gate: bool, lfg_closeout: bool, lfg_gate: bool, + lfg_gate_watch: bool, lfg_preflight: bool, lfg_preflight_watch: bool, lfg_refresh: bool, @@ -2760,6 +4018,8 @@ def _resolve_lfg_mode( ) -> str | None: if lfg_merge_watch or (lfg_merge_gate and lfg_pr_watch): return "merge_watch" + if lfg_gate_watch or (lfg_gate and lfg_preflight_watch): + return "gate_watch" if lfg_preflight_watch: return "preflight_watch" if lfg_pr_watch: @@ -2807,6 +4067,7 @@ def main() -> None: "Examples:\n" " python3 .github/scripts/local_verify_pypi_slice.py\n" " python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate\n" + " python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch\n" " python3 .github/scripts/local_verify_pypi_slice.py --lfg-merge-gate\n" " python3 .github/scripts/local_verify_pypi_slice.py --lfg-merge-gate --lfg-pr-watch\n" " python3 .github/scripts/local_verify_pypi_slice.py --lfg-merge-watch\n" @@ -2938,6 +4199,11 @@ def main() -> None: action="store_true", help="Shorthand for --lfg-preflight --strict-defer-exit (full JSON then exit 2 when deferred)", ) + parser.add_argument( + "--lfg-gate-watch", + action="store_true", + help="Shorthand for --lfg-gate --lfg-preflight-watch (poll until defer clears then gate exit)", + ) parser.add_argument( "--lfg-merge-watch", action="store_true", @@ -3015,6 +4281,10 @@ def main() -> None: args.lfg_merge_gate = True args.lfg_pr_watch = True + if args.lfg_gate_watch: + args.lfg_gate = True + args.lfg_preflight_watch = True + if args.lfg_preflight_watch: args.lfg_preflight = True args.strict_defer_exit = True @@ -3147,11 +4417,14 @@ def main() -> None: if args.prefetch_git and args.compare_checkpoint and not args.lfg_preflight_watch: prefetch_result = _git_prefetch_origin_master() if args.lfg_preflight_watch: + watch_label = "gate" if args.lfg_gate_watch else "preflight" status = _watch_lfg_preflight_defer( targets=targets, prefetch_git=args.prefetch_git, interval_sec=max(0.0, args.watch_interval), timeout_sec=max(0.0, args.watch_timeout), + watch_label=watch_label, + flat_keys_heartbeat_polls=max(0, args.watch_heartbeat_polls), ) deferred = bool(status.get("lfg_deferred")) if deferred: @@ -3216,6 +4489,7 @@ def main() -> None: lfg_merge_gate=args.lfg_merge_gate, lfg_closeout=args.lfg_closeout, lfg_gate=args.lfg_gate, + lfg_gate_watch=args.lfg_gate_watch, lfg_preflight=args.lfg_preflight, lfg_preflight_watch=args.lfg_preflight_watch, lfg_refresh=args.lfg_refresh, @@ -3230,7 +4504,7 @@ def main() -> None: briefing, 2, ): - _emit_lfg_agent_briefing_stderr(briefing) + _emit_lfg_agent_briefing_stderr(status) _print_ci_status(status, as_json=args.json) if not status["gh_ok"]: sys.exit(1) @@ -3268,6 +4542,7 @@ def main() -> None: lfg_merge_gate=args.lfg_merge_gate, lfg_closeout=args.lfg_closeout, lfg_gate=args.lfg_gate, + lfg_gate_watch=args.lfg_gate_watch, lfg_preflight=args.lfg_preflight, lfg_preflight_watch=args.lfg_preflight_watch, lfg_refresh=args.lfg_refresh, @@ -3378,7 +4653,7 @@ def main() -> None: briefing, exit_code, ): - _emit_lfg_agent_briefing_stderr(briefing) + _emit_lfg_agent_briefing_stderr(status) _print_ci_status(status, as_json=args.json) if not status["gh_ok"]: sys.exit(1) diff --git a/Libraries/PyKotor/tests/test_utility/test_local_verify_checkpoint.py b/Libraries/PyKotor/tests/test_utility/test_local_verify_checkpoint.py index d649daa2e..9affc14fa 100644 --- a/Libraries/PyKotor/tests/test_utility/test_local_verify_checkpoint.py +++ b/Libraries/PyKotor/tests/test_utility/test_local_verify_checkpoint.py @@ -8,7 +8,7 @@ import subprocess import sys import unittest -from datetime import date +from datetime import date, datetime, timedelta, timezone from pathlib import Path from typing import Any from unittest import mock @@ -457,7 +457,7 @@ def test_build_proceed_hint_fc_active_closeout(self) -> None: }, blocked="deferred", ) - self.assertIn("--lfg-preflight", hint) + self.assertIn("--lfg-gate-watch", hint) self.assertIn("terminal", hint) def test_replace_frontmatter_field(self) -> None: @@ -496,2224 +496,4959 @@ def test_patch_plan020_updates_verification_rows(self) -> None: self.assertTrue(changes["forward_commits_row"]) self.assertTrue(changes["plans_index"]) self.assertIn("https://example.com/10", patched) - self.assertIn("019–114", patched) + self.assertIn("019–214", patched) - def test_dedupe_preserve_order(self) -> None: + def test_preflight_watch_summary_heartbeat_flat_stderr_parts(self) -> None: + parts = mod._preflight_watch_summary_heartbeat_flat_stderr_parts( + { + "flat_unchanged": 12, + "heartbeat_every": 12, + "flat_hb_total": 1, + } + ) + joined = " ".join(parts) + self.assertIn("flat_hb_total=1", joined) + self.assertNotIn("flat_unchanged=", joined) + + def test_preflight_watch_summary_unchanged_flat_stderr_parts(self) -> None: + parts = mod._preflight_watch_summary_unchanged_flat_stderr_parts( + { + "flat_unchanged": 2, + "max_flat_unchanged": 1, + "heartbeat_every": 12, + } + ) + joined = " ".join(parts) + self.assertIn("flat_unchanged=2", joined) + self.assertIn("max_flat_unchanged=1", joined) + self.assertIn("heartbeat_every=12", joined) + self.assertNotIn("flat_hb_total=", joined) + + def test_preflight_heartbeat_every_for_stderr_emits_when_unchanged(self) -> None: self.assertEqual( - mod._dedupe_preserve_order(["a", "b", "a", "c", "b"]), - ["a", "b", "c"], + mod._preflight_heartbeat_every_for_stderr( + {"flat_unchanged": 2, "heartbeat_every": 12} + ), + 12, ) - def test_summarize_pr_checks_dedupes_pending_names(self) -> None: - summary = mod._summarize_pr_checks( - [ - {"name": "Analyze (python)", "conclusion": "", "status": "QUEUED"}, - {"name": "Analyze (python)", "conclusion": "", "status": "IN_PROGRESS"}, - {"name": "build", "conclusion": "", "status": "QUEUED"}, - ] + def test_preflight_heartbeat_every_for_stderr_omits_without_unchanged(self) -> None: + self.assertEqual( + mod._preflight_heartbeat_every_for_stderr({"heartbeat_every": 12}), + 0, ) - self.assertEqual(summary["pending_checks"], ["Analyze (python)", "build"]) - self.assertEqual(summary["checks_pending"], 3) - def test_summarize_pr_checks_in_progress_and_details(self) -> None: - summary = mod._summarize_pr_checks( - [ - { - "name": "build", - "conclusion": "", - "status": "IN_PROGRESS", - "detailsUrl": "https://example.com/job/1", - "workflowName": "CI", - }, + def test_preflight_flat_hb_total_for_stderr_emits_when_gate_passes(self) -> None: + self.assertEqual( + mod._preflight_flat_hb_total_for_stderr( { - "name": "build", - "conclusion": "", - "status": "QUEUED", - "detailsUrl": "https://example.com/job/2", - "workflowName": "CI", - }, + "flat_unchanged": 12, + "heartbeat_every": 12, + "flat_hb_total": 1, + } + ), + 1, + ) + + def test_preflight_flat_hb_total_for_stderr_omits_when_unchanged_below_interval(self) -> None: + self.assertEqual( + mod._preflight_flat_hb_total_for_stderr( { - "name": "lint", - "conclusion": "FAILURE", - "status": "COMPLETED", - "detailsUrl": "https://example.com/job/3", - "workflowName": "Lint", - }, - ] + "flat_unchanged": 5, + "heartbeat_every": 12, + "flat_hb_total": 1, + } + ), + 0, ) - self.assertEqual(summary["checks_in_progress"], 1) - self.assertEqual(summary["checks_queued"], 1) - self.assertEqual(summary["checks_pending"], 2) - self.assertEqual(len(summary["pending_check_details"]), 1) - self.assertEqual(len(summary["in_progress_check_details"]), 1) - self.assertEqual(summary["in_progress_check_details"][0]["details_url"], "https://example.com/job/1") - self.assertEqual(len(summary["failed_check_details"]), 1) - self.assertEqual(summary["failed_check_details"][0]["workflow"], "Lint") - def test_summarize_pr_checks_ci_progress(self) -> None: - summary = mod._summarize_pr_checks( - [ - {"name": "a", "conclusion": "SUCCESS", "status": "COMPLETED"}, - {"name": "b", "conclusion": "SKIPPED", "status": "COMPLETED"}, - {"name": "c", "conclusion": "", "status": "QUEUED"}, - {"name": "d", "conclusion": "FAILURE", "status": "COMPLETED"}, - ] + def test_preflight_max_flat_unchanged_resolver(self) -> None: + self.assertEqual(mod._preflight_max_flat_unchanged({"max_flat_unchanged": 2}), 2) + self.assertEqual(mod._preflight_max_flat_unchanged({}), 0) + + def test_preflight_max_flat_unchanged_for_stderr_emits_when_peak_below_total(self) -> None: + self.assertEqual( + mod._preflight_max_flat_unchanged_for_stderr( + {"flat_unchanged": 2, "max_flat_unchanged": 1} + ), + 1, ) - progress = summary["pr_ci_progress"] - self.assertEqual(progress["total"], 4) - self.assertEqual(progress["terminal"], 3) - self.assertEqual(progress["remaining"], 1) - self.assertEqual(progress["completion_percent"], 75) - def test_summarize_pr_checks_status_context(self) -> None: - summary = mod._summarize_pr_checks( - [ - { - "context": "ci/circleci", - "state": "SUCCESS", - "targetUrl": "https://example.com/status/1", - }, - { - "context": "ci/travis", - "state": "PENDING", - "targetUrl": "https://example.com/status/2", - }, - ] + def test_preflight_max_flat_unchanged_for_stderr_omits_when_peak_equals_total(self) -> None: + self.assertEqual( + mod._preflight_max_flat_unchanged_for_stderr( + {"flat_unchanged": 2, "max_flat_unchanged": 2} + ), + 0, ) - self.assertEqual(summary["checks_success"], 1) - self.assertEqual(summary["checks_pending"], 1) - self.assertIn("ci/travis", summary["pending_checks"]) - self.assertFalse(summary["pr_merge_ready"]) - def test_check_detail_record_uses_context(self) -> None: - detail = mod._check_detail_record( - {"context": "ci/travis", "targetUrl": "https://example.com/t", "state": "PENDING"} + def test_preflight_watch_summary_flat_stderr_parts_watch_heartbeat_alias(self) -> None: + parts = mod._preflight_watch_summary_flat_stderr_parts( + { + "flat_unchanged": 2, + "watch_heartbeat_polls": 12, + } ) - self.assertEqual(detail["name"], "ci/travis") - self.assertEqual(detail["details_url"], "https://example.com/t") + self.assertIn("heartbeat_every=12", " ".join(parts)) - def test_format_watch_poll_line_includes_percent(self) -> None: - line = mod._format_watch_poll_line( + def test_resolve_preflight_flat_keys_heartbeats_prefers_status(self) -> None: + history = [{"flat_hb_total": 2}] + status: dict[str, Any] = {"preflight_flat_keys_heartbeats": 3} + self.assertEqual(mod._resolve_preflight_flat_keys_heartbeats(status, history), 3) + + def test_resolve_preflight_flat_keys_heartbeats_history_fallback(self) -> None: + history = [{"flat_hb": 1}, {"flat_hb_total": 2}] + self.assertEqual(mod._resolve_preflight_flat_keys_heartbeats({}, history), 2) + + def test_resolve_preflight_unchanged_flat_keys_polls_prefers_count(self) -> None: + history = [ + {"flat_keys": ["primary_action"]}, + {"flat_keys": ["primary_action"], "flat_unchanged": 1}, + ] + self.assertEqual(mod._resolve_preflight_unchanged_flat_keys_polls(history), 1) + + def test_resolve_preflight_unchanged_flat_keys_polls_max_streak_fallback(self) -> None: + history = [{"flat_unchanged": 2}, {"flat_unchanged": 1}] + self.assertEqual(mod._resolve_preflight_unchanged_flat_keys_polls(history), 2) + + def test_build_preflight_watch_summary_flat_unchanged_max_streak_fallback(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [ + {"flat_unchanged": 2}, + {"flat_unchanged": 1}, + ], + "lfg_preflight_watch_result": "timeout", + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("unchanged_flat_keys_polls"), 2) + self.assertEqual(summary.get("flat_unchanged"), 2) + self.assertEqual(summary.get("max_flat_unchanged"), 2) + + def test_preflight_watch_summary_flat_stderr_parts_unchanged(self) -> None: + parts = mod._preflight_watch_summary_flat_stderr_parts( { - "checks_pending": 2, - "checks_in_progress": 1, - "checks_failed": 0, - "checks_success": 5, - "pr_ci_progress": {"completion_percent": 62}, + "flat_unchanged": 2, + "max_flat_unchanged": 1, + "heartbeat_every": 12, } ) - self.assertIn("complete=62%", line) - self.assertIn("skipped=", line) + joined = " ".join(parts) + self.assertIn("flat_unchanged=2", joined) + self.assertIn("max_flat_unchanged=1", joined) + self.assertIn("heartbeat_every=12", joined) + self.assertNotIn("flat_hb_total=", joined) + + def test_preflight_watch_summary_flat_stderr_parts_heartbeat(self) -> None: + parts = mod._preflight_watch_summary_flat_stderr_parts( + { + "flat_unchanged": 12, + "heartbeat_every": 12, + "flat_hb_total": 1, + "unchanged_flat_keys_polls": 12, + } + ) + joined = " ".join(parts) + self.assertIn("flat_unchanged=12", joined) + self.assertIn("flat_hb_total=1", joined) + self.assertNotIn("flat_hb=", joined) + + def test_preflight_watch_poll_flat_stderr_parts_unchanged(self) -> None: + parts = mod._preflight_watch_poll_flat_stderr_parts( + ["flat_keys=primary_action", "flat_fields=1"], + flat_keys_unchanged=True, + flat_keys_unchanged_streak=3, + flat_keys_heartbeat_polls=12, + ) + self.assertIn("flat_unchanged=3", parts) + self.assertNotIn("flat_keys=primary_action", " ".join(parts)) + self.assertIn("heartbeat_every=12", parts) + + def test_preflight_watch_poll_flat_stderr_parts_heartbeat(self) -> None: + parts = mod._preflight_watch_poll_flat_stderr_parts( + ["flat_keys=primary_action"], + flat_keys_unchanged=True, + flat_keys_unchanged_streak=12, + flat_keys_heartbeat_polls=12, + flat_keys_heartbeat_count=2, + ) + joined = " ".join(parts) + self.assertIn("flat_keys=primary_action", joined) + self.assertIn("flat_hb=2", joined) + self.assertIn("heartbeat_every=12", joined) + self.assertNotIn("flat_unchanged=", joined) - def test_watch_snapshot_progress_key_and_compact_line(self) -> None: - snapshot = { - "completion_percent": 4, - "checks_pending": 27, - "checks_in_progress": 0, - "checks_success": 1, - "checks_failed": 0, + def test_format_preflight_watch_poll_line_flat_unchanged_streak(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], + }, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, } - self.assertEqual( - mod._watch_snapshot_progress_key(snapshot), - (4, 27, 0, 1, 0), + first_status = dict(status) + mod._format_preflight_watch_poll_line(1, first_status) + previous = mod._lfg_flat_field_keys_present_stderr(first_status) + line = mod._format_preflight_watch_poll_line( + 4, + dict(status), + previous_flat_keys=previous, + flat_keys_unchanged_streak=3, ) - self.assertIn("unchanged complete=4%", mod._format_compact_watch_poll_line(snapshot)) + self.assertIn("flat_unchanged=3", line) + self.assertNotIn("flat_unchanged=1", line) - def test_count_unchanged_watch_polls(self) -> None: + def test_max_preflight_flat_unchanged_streak(self) -> None: history = [ - {"completion_percent": 4, "checks_pending": 27, "checks_in_progress": 0, "checks_success": 1, "checks_failed": 0}, - {"completion_percent": 4, "checks_pending": 27, "checks_in_progress": 0, "checks_success": 1, "checks_failed": 0}, - {"completion_percent": 8, "checks_pending": 25, "checks_in_progress": 1, "checks_success": 2, "checks_failed": 0}, - {"completion_percent": 8, "checks_pending": 25, "checks_in_progress": 1, "checks_success": 2, "checks_failed": 0}, + {"flat_keys": ["a"]}, + {"flat_keys": ["a"], "flat_unchanged": 1}, + {"flat_keys": ["a", "b"]}, + {"flat_keys": ["a", "b"], "flat_unchanged": 1}, ] - self.assertEqual(mod._count_unchanged_watch_polls(history), 2) + self.assertEqual(mod._count_unchanged_preflight_flat_keys_polls(history), 2) + self.assertEqual(mod._max_preflight_flat_unchanged_streak(history), 1) - def test_should_emit_watch_heartbeat(self) -> None: - self.assertFalse( - mod._should_emit_watch_heartbeat(True, 11, 12), + def test_build_preflight_watch_summary_max_flat_unchanged(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [ + {"flat_keys": ["a"]}, + {"flat_keys": ["a"], "flat_unchanged": 1}, + {"flat_keys": ["a", "b"]}, + {"flat_keys": ["a", "b"], "flat_unchanged": 1}, + ], + "lfg_preflight_watch_result": "timeout", + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("flat_unchanged"), 2) + self.assertEqual(summary.get("max_flat_unchanged"), 1) + + def test_format_preflight_watch_summary_line_max_flat_unchanged(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "polls": 4, + "lfg_preflight_watch_result": "timeout", + "flat_unchanged": 2, + "max_flat_unchanged": 1, + "watch_heartbeat_polls": 12, + }, ) + self.assertIn("flat_unchanged=2", line) + self.assertIn("max_flat_unchanged=1", line) + + def test_watch_lfg_preflight_defer_history_flat_unchanged_streak(self) -> None: + deferred_status: dict[str, Any] = { + "gh_ok": True, + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "fc_active_pending", + }, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], + }, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + with patch.object( + mod, "_ci_status", side_effect=[deferred_status, deferred_status, deferred_status] + ): + with patch.object(mod, "_refine_lfg_checkpoint"): + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + with patch.object(mod.time, "sleep"): + with patch.object( + mod.time, + "monotonic", + side_effect=[0.0, 0.0, 0.0, 0.0, 100.0, 100.0], + ): + status = mod._watch_lfg_preflight_defer( + targets=["solution"], + prefetch_git=False, + interval_sec=0.0, + timeout_sec=5.0, + flat_keys_heartbeat_polls=12, + ) + history = status.get("preflight_watch_history") or [] + self.assertEqual(len(history), 3) + self.assertNotIn("flat_unchanged", history[0]) + self.assertEqual(history[1].get("flat_unchanged"), 1) + self.assertEqual(history[2].get("flat_unchanged"), 2) + summary = status.get("preflight_watch_summary") or {} + self.assertEqual(summary.get("max_flat_unchanged"), 2) + self.assertNotIn("max_flat_unchanged=", mod._format_preflight_watch_summary_line(summary)) + + def test_watch_lfg_preflight_defer_history_flat_hb_cumulative(self) -> None: + deferred_status: dict[str, Any] = { + "gh_ok": True, + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "fc_active_pending", + }, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], + }, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + with patch.object( + mod, "_ci_status", side_effect=[deferred_status, deferred_status, deferred_status] + ): + with patch.object(mod, "_refine_lfg_checkpoint"): + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + with patch.object(mod.time, "sleep"): + with patch.object( + mod.time, + "monotonic", + side_effect=[0.0, 0.0, 0.0, 0.0, 100.0, 100.0], + ): + status = mod._watch_lfg_preflight_defer( + targets=["solution"], + prefetch_git=False, + interval_sec=0.0, + timeout_sec=5.0, + flat_keys_heartbeat_polls=1, + ) + history = status.get("preflight_watch_history") or [] + self.assertEqual(len(history), 3) + self.assertNotIn("flat_hb", history[0]) + self.assertNotIn("flat_hb_total", history[0]) + self.assertEqual(history[1].get("flat_hb"), 1) + self.assertEqual(history[1].get("flat_hb_total"), 1) + self.assertEqual(history[2].get("flat_hb"), 2) + self.assertEqual(history[2].get("flat_hb_total"), 2) + summary = status.get("preflight_watch_summary") or {} + self.assertEqual(summary.get("flat_hb_total"), 2) + + def test_build_preflight_watch_summary_flat_unchanged_alias(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [ + {"flat_keys": ["primary_action"]}, + {"flat_keys": ["primary_action"]}, + ], + "lfg_preflight_watch_result": "timeout", + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("unchanged_flat_keys_polls"), 1) + self.assertEqual(summary.get("flat_unchanged"), 1) + + def test_should_emit_preflight_flat_keys_heartbeat_summary_flat_unchanged(self) -> None: self.assertTrue( - mod._should_emit_watch_heartbeat(True, 12, 12), + mod._should_emit_preflight_flat_keys_heartbeat_summary( + { + "flat_hb": 1, + "flat_unchanged": 12, + "heartbeat_every": 12, + } + ) ) - self.assertFalse( - mod._should_emit_watch_heartbeat(True, 12, 0), + + def test_build_preflight_watch_summary_flat_hb_alias(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [], + "lfg_preflight_watch_result": "timeout", + "preflight_flat_keys_heartbeats": 2, + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("flat_keys_heartbeat_polls"), 2) + self.assertEqual(summary.get("flat_hb"), 2) + self.assertEqual(summary.get("flat_hb_total"), 2) + + def test_preflight_flat_keys_heartbeat_count_prefers_flat_hb_total(self) -> None: + self.assertEqual( + mod._preflight_flat_keys_heartbeat_count({"flat_hb_total": 3, "flat_hb": 2}), + 3, ) - def test_watch_pr_merge_status_heartbeat_poll(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - pending_status = { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "checks_pending": 27, - "checks_in_progress": 0, - "checks_success": 1, - "checks_failed": 0, - "checks_skipped": 0, - "pr_ci_progress": {"completion_percent": 4, "remaining": 27, "total": 28}, - "pending_check_details": [ - { - "name": "label", - "started_at": "2026-05-27T21:30:00Z", - "workflow": "CI", - "details_url": "", - }, + def test_max_preflight_flat_hb_total_from_history(self) -> None: + history = [ + {"flat_keys": ["primary_action"]}, + {"flat_hb": 1}, + {"flat_hb_total": 2}, + ] + self.assertEqual(mod._max_preflight_flat_hb_total(history), 2) + + def test_build_preflight_watch_summary_flat_hb_total_history_fallback(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [ + {"flat_keys": ["primary_action"]}, + {"flat_hb_total": 1}, + {"flat_hb": 2}, ], - "pr_merge_ready": False, + "lfg_preflight_watch_result": "timeout", } - calls = {"n": 0} + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("flat_hb_total"), 2) + self.assertEqual(summary.get("flat_hb"), 2) + self.assertEqual(summary.get("flat_keys_heartbeat_polls"), 2) - def fetch_side() -> dict[str, Any]: - calls["n"] += 1 - if calls["n"] >= 14: - return { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "pr_merge_ready": True, - "lfg_merge_blocked": None, + def test_should_emit_preflight_flat_keys_heartbeat_summary_flat_hb(self) -> None: + self.assertTrue( + mod._should_emit_preflight_flat_keys_heartbeat_summary( + { + "flat_hb": 1, + "unchanged_flat_keys_polls": 12, + "heartbeat_every": 12, } - return dict(pending_status) - - with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): - with patch.object( - mod, - "_fetch_pr_checks_crosscheck", - return_value={ - "ok": True, - "gh_checks_total": 26, - "rollup_checks_total": 28, - "rollup_vs_gh_delta": 2, - "gh_state_counts": {"QUEUED": 25}, - }, - ): - with patch.object(mod.time, "sleep"): - with patch("sys.stderr", new_callable=io.StringIO) as err: - mod._watch_pr_merge_status( - status, - interval_sec=0.0, - timeout_sec=60.0, - stall_polls=99, - heartbeat_polls=12, - ) - output = err.getvalue() - self.assertIn("PR watch poll 13:", output) - poll13 = output.split("PR watch poll 13:")[1].split("\n")[0] - self.assertIn("heartbeat=1", poll13) - self.assertIn("success=", poll13) - self.assertIn("rollup_delta=", poll13) - summary = status.get("pr_watch_summary") or {} - self.assertEqual(summary.get("heartbeat_polls"), 1) + ) + ) - def test_watch_pr_merge_status_compact_unchanged_polls(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - pending_status = { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "checks_pending": 27, - "checks_in_progress": 0, - "checks_success": 1, - "checks_failed": 0, - "checks_skipped": 0, - "pr_ci_progress": {"completion_percent": 4, "remaining": 27, "total": 28}, - "pending_check_details": [ - { - "name": "label", - "started_at": "2026-05-27T21:30:00Z", - "workflow": "CI", - "details_url": "", - }, - ], - "pr_merge_ready": False, + def test_build_preflight_watch_summary_heartbeat_every_alias(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [], + "lfg_preflight_watch_result": "timeout", + "preflight_watch_heartbeat_polls": 12, } - calls = {"n": 0} + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("watch_heartbeat_polls"), 12) + self.assertEqual(summary.get("heartbeat_every"), 12) - def fetch_side() -> dict[str, Any]: - calls["n"] += 1 - if calls["n"] >= 3: - return { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "pr_merge_ready": True, - "lfg_merge_blocked": None, + def test_should_emit_preflight_flat_keys_heartbeat_summary_heartbeat_every(self) -> None: + self.assertTrue( + mod._should_emit_preflight_flat_keys_heartbeat_summary( + { + "flat_keys_heartbeat_polls": 1, + "unchanged_flat_keys_polls": 12, + "heartbeat_every": 12, } - return dict(pending_status) + ) + ) - with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): - with patch.object(mod.time, "sleep"): - with patch("sys.stderr", new_callable=io.StringIO) as err: - mod._watch_pr_merge_status( - status, - interval_sec=0.0, - timeout_sec=60.0, - stall_polls=99, - ) - output = err.getvalue() - self.assertIn("PR watch poll 2: unchanged", output) - self.assertIn("queue_age=", output) - self.assertNotIn("rollup_delta=", output.split("PR watch poll 2:")[1].split("\n")[0]) - summary = status.get("pr_watch_summary") or {} - self.assertEqual(summary.get("unchanged_polls"), 1) + def test_lfg_flat_field_mirror_stderr_parts(self) -> None: + parts = mod._lfg_flat_field_mirror_stderr_parts( + { + "lfg_flat_field_values": { + "primary_action": "gate_watch", + "fc_run_id": 2, + }, + "lfg_flat_field_keys_present": ["primary_action", "fc_run_id"], + } + ) + self.assertTrue(any(part.startswith("flat_fields=") for part in parts)) + self.assertTrue(any(part.startswith("flat_keys=") for part in parts)) - def test_compute_lfg_exit_code_no_open_pr(self) -> None: - code = mod._compute_lfg_exit_code( + def test_format_preflight_watch_summary_line_watch_heartbeat_polls(self) -> None: + line = mod._format_preflight_watch_summary_line( { - "gh_ok": True, - "lfg_track_complete": True, - "lfg_merge_blocked": "no_open_pr", - "pr_merge_status": {"ok": False}, + "polls": 5, + "lfg_preflight_watch_result": "timeout", + "unchanged_flat_keys_polls": 3, + "watch_heartbeat_polls": 12, }, - deferred=False, - strict_defer_exit=False, - strict_pr_ci_exit=True, - dispatch_on_proceed=False, - execute=False, - sync_docs_after_dispatch=False, - write=False, - lfg_refresh=False, + watch_label="gate", ) - self.assertEqual(code, 3) - - def test_apply_pr_merge_status_no_open_pr(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={"ok": False, "error": "no open PR"}, - ): - mod._apply_pr_merge_status(status) - self.assertEqual(status["lfg_merge_blocked"], "no_open_pr") + self.assertIn("flat_unchanged=3", line) + self.assertNotIn("unchanged_flat_keys_polls=", line) + self.assertIn("heartbeat_every=12", line) + self.assertNotIn("watch_heartbeat_polls=", line) + + def test_format_preflight_watch_summary_line_omits_watch_heartbeat_without_unchanged( + self, + ) -> None: + line = mod._format_preflight_watch_summary_line( + { + "polls": 2, + "lfg_preflight_watch_result": "proceed", + "unchanged_flat_keys_polls": 0, + "watch_heartbeat_polls": 12, + }, + ) + self.assertNotIn("watch_heartbeat_polls=", line) + self.assertNotIn("heartbeat_every=", line) + self.assertNotIn("flat_unchanged=", line) - def test_build_merge_actions_with_number(self) -> None: - actions = mod._build_merge_actions(308) - self.assertIn("gh pr checks 308 --watch", actions["watch_checks"]) - self.assertIn("gh pr merge 308 --squash --auto", actions["merge_squash_auto"]) + def test_build_preflight_watch_summary_watch_heartbeat_polls(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [], + "lfg_preflight_watch_result": "timeout", + "preflight_watch_heartbeat_polls": 12, + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("watch_heartbeat_polls"), 12) + self.assertEqual(summary.get("heartbeat_every"), 12) - def test_fetch_pr_merge_status_merged(self) -> None: - payload = { - "number": 308, - "url": "https://example.com/pr/308", - "state": "MERGED", - "mergeable": "UNKNOWN", - "statusCheckRollup": [], + def test_build_preflight_watch_summary_omits_heartbeat_every_when_zero(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [], + "lfg_preflight_watch_result": "timeout", + "preflight_watch_heartbeat_polls": 0, } - with patch.object( - mod.subprocess, - "run", - return_value=mock.Mock(returncode=0, stdout=json.dumps(payload), stderr=""), - ): - result = mod._fetch_pr_merge_status() - self.assertEqual(result["lfg_merge_blocked"], "pr_merged") - self.assertFalse(result["pr_merge_ready"]) + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("watch_heartbeat_polls"), 0) + self.assertNotIn("heartbeat_every", summary) - def test_apply_pr_merge_status_merge_actions_and_next_pending(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "number": 308, - "url": "https://example.com/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pending_check_details": [ - { - "name": "build", - "details_url": "https://example.com/job/1", - "workflow": "CI", - } - ], - "pr_merge_ready": False, - }, - ): - mod._apply_pr_merge_status(status) - self.assertIn("watch_checks", status["merge_actions"]) - self.assertEqual(status["next_pending_check"]["name"], "build") + def test_should_emit_preflight_flat_keys_heartbeat_summary(self) -> None: + self.assertTrue( + mod._should_emit_preflight_flat_keys_heartbeat_summary( + { + "flat_keys_heartbeat_polls": 1, + "unchanged_flat_keys_polls": 12, + "watch_heartbeat_polls": 12, + } + ) + ) + self.assertFalse( + mod._should_emit_preflight_flat_keys_heartbeat_summary( + { + "flat_keys_heartbeat_polls": 1, + "unchanged_flat_keys_polls": 5, + "watch_heartbeat_polls": 12, + } + ) + ) - def test_pick_next_pending_check_prefers_in_progress(self) -> None: - picked = mod._pick_next_pending_check( + def test_format_preflight_watch_summary_line_omits_early_heartbeat_polls(self) -> None: + line = mod._format_preflight_watch_summary_line( { - "in_progress_check_details": [ - {"name": "running", "details_url": "https://example.com/r", "workflow": "CI"}, - ], - "pending_check_details": [ - {"name": "queued", "details_url": "https://example.com/q", "workflow": "CI"}, - ], - } + "polls": 6, + "lfg_preflight_watch_result": "timeout", + "flat_keys_heartbeat_polls": 1, + "unchanged_flat_keys_polls": 5, + "watch_heartbeat_polls": 12, + }, + watch_label="gate", ) - self.assertEqual(picked["name"], "running") + self.assertNotIn("flat_keys_heartbeat_polls=", line) + self.assertNotIn("flat_hb=", line) + self.assertNotIn("flat_hb_total=", line) - def test_apply_pr_merge_status_next_failed_check(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "number": 308, - "url": "https://example.com/pr/308", - "lfg_merge_blocked": "pr_checks_failed", - "failed_check_details": [ - { - "name": "lint", - "details_url": "https://example.com/job/fail", - "workflow": "Lint", - } - ], - "pr_merge_ready": False, + def test_format_preflight_watch_poll_line_flat_keys_heartbeat(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], }, - ): - mod._apply_pr_merge_status(status) - self.assertEqual(status["next_failed_check"]["name"], "lint") - self.assertEqual(status["merge_actions"]["list_failed"], "gh pr checks 308 --failed") + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + first_status = dict(status) + mod._format_preflight_watch_poll_line(1, first_status) + previous = mod._lfg_flat_field_keys_present_stderr(first_status) + line = mod._format_preflight_watch_poll_line( + 13, + dict(status), + previous_flat_keys=previous, + flat_keys_unchanged_streak=12, + flat_keys_heartbeat_polls=12, + flat_keys_heartbeat_count=2, + ) + self.assertIn("flat_keys=", line) + self.assertIn("flat_hb=2", line) + self.assertIn("heartbeat_every=12", line) + self.assertNotIn("flat_unchanged=1", line) + self.assertNotIn("flat_keys_heartbeat=", line) - def test_compute_lfg_exit_reason_merge_ready(self) -> None: - reason = mod._compute_lfg_exit_reason( - {"pr_merge_status": {"pr_merge_ready": True}}, - 0, - deferred=False, + def test_build_preflight_watch_summary_flat_keys_heartbeat_polls(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [{"flat_keys": ["primary_action"]}], + "lfg_preflight_watch_result": "timeout", + "preflight_flat_keys_heartbeats": 2, + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("flat_keys_heartbeat_polls"), 2) + + def test_count_unchanged_preflight_flat_keys_polls(self) -> None: + history = [ + {"flat_keys": ["primary_action", "fc_run_id"]}, + {"flat_keys": ["primary_action", "fc_run_id"]}, + {"flat_keys": ["primary_action", "fc_run_id", "verify_run_id"]}, + ] + self.assertEqual(mod._count_unchanged_preflight_flat_keys_polls(history), 1) + + def test_build_preflight_watch_summary_unchanged_flat_keys(self) -> None: + status: dict[str, Any] = { + "preflight_watch_history": [ + {"flat_keys": ["primary_action", "fc_run_id"]}, + {"flat_keys": ["primary_action", "fc_run_id"]}, + ], + "lfg_preflight_watch_result": "timeout", + } + summary = mod._build_preflight_watch_summary(status) + self.assertEqual(summary.get("unchanged_flat_keys_polls"), 1) + + def test_format_preflight_watch_summary_line_unchanged_flat_keys(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 3, + "watch_duration_sec": 12.0, + "unchanged_flat_keys_polls": 2, + "watch_heartbeat_polls": 12, + } ) - self.assertEqual(reason, "merge_ready") + self.assertIn("flat_unchanged=2", line) + self.assertNotIn("unchanged_flat_keys_polls=", line) + self.assertIn("heartbeat_every=12", line) + self.assertNotIn("watch_heartbeat_polls=", line) - def test_compute_lfg_exit_reason_monitoring_complete(self) -> None: - reason = mod._compute_lfg_exit_reason( - {"lfg_track_complete": True, "pr_merge_status": {"pr_merge_ready": False}}, - 0, - deferred=False, + def test_format_preflight_watch_summary_line_flat_keys_heartbeat_polls(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "polls": 13, + "result": "timeout", + "flat_keys_heartbeat_polls": 1, + "unchanged_flat_keys_polls": 12, + "watch_heartbeat_polls": 12, + }, + watch_label="gate", ) - self.assertEqual(reason, "monitoring_complete") + self.assertIn("flat_hb_total=1", line) + self.assertNotIn("flat_hb=", line) + self.assertNotIn("flat_keys_heartbeat_polls=", line) - def test_summarize_pr_checks_skipped_not_pending(self) -> None: - summary = mod._summarize_pr_checks( - [ - {"name": "label", "conclusion": "SKIPPED", "status": "COMPLETED"}, - {"name": "build", "conclusion": "", "status": "QUEUED"}, - ] - ) - self.assertEqual(summary["checks_skipped"], 1) - self.assertEqual(summary["checks_pending"], 1) - self.assertEqual(summary["pending_checks"], ["build"]) - self.assertFalse(summary["pr_merge_ready"]) - - def test_summarize_pr_checks_merge_ready(self) -> None: - summary = mod._summarize_pr_checks( - [ - {"name": "test", "conclusion": "SUCCESS", "status": "COMPLETED"}, - {"name": "lint", "conclusion": "SKIPPED", "status": "COMPLETED"}, - ] + def test_format_preflight_watch_poll_line_omits_unchanged_flat_keys(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], + }, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + first_status = dict(status) + first = mod._format_preflight_watch_poll_line(1, first_status) + self.assertIn("flat_keys=", first) + self.assertNotIn("flat_unchanged=1", first) + previous = mod._lfg_flat_field_keys_present_stderr(first_status) + second = mod._format_preflight_watch_poll_line( + 2, + dict(status), + previous_flat_keys=previous, ) - self.assertTrue(summary["pr_merge_ready"]) - self.assertIsNone(summary["lfg_merge_blocked"]) - - def test_apply_pr_merge_status_failed_names(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "number": 308, - "url": "https://example.com/pr/1", - "lfg_merge_blocked": "pr_checks_failed", - "failed_checks": ["Check File Sizes", "devskim"], - "pr_merge_ready": False, + self.assertNotIn("flat_keys=", second) + self.assertNotIn("flat_fields=", second) + self.assertIn("flat_unchanged=1", second) + self.assertNotIn("flat_unchanged=true", second) + self.assertIn("heartbeat_every=12", second) + + def test_format_preflight_watch_poll_line_flat_keys_changed(self) -> None: + base: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "primary_action": "gate_watch", + "fc_run_id": 2, + "lfg_flat_field_keys_present": ["primary_action", "fc_run_id"], + "lfg_flat_field_values": { + "primary_action": "gate_watch", + "fc_run_id": 2, }, - ): - mod._apply_pr_merge_status(status) - self.assertIn("Check File Sizes", status["merge_hint"]) - self.assertIn("gh pr checks 308 --failed", status["merge_hint"]) - self.assertEqual(status["lfg_merge_blocked"], "pr_checks_failed") - - def test_fetch_pr_merge_status_conflicts(self) -> None: - payload = { - "number": 308, - "url": "https://example.com/pr/308", - "state": "OPEN", - "mergeable": "CONFLICTING", - "statusCheckRollup": [ - {"name": "build", "conclusion": "SUCCESS", "status": "COMPLETED"}, - ], } - with patch.object( - mod.subprocess, - "run", - return_value=mock.Mock(returncode=0, stdout=json.dumps(payload), stderr=""), - ): - result = mod._fetch_pr_merge_status() - self.assertFalse(result["pr_merge_ready"]) - self.assertEqual(result["lfg_merge_blocked"], "pr_merge_conflicts") + poll_status = dict(base) + line = mod._format_preflight_watch_poll_line( + 2, + poll_status, + previous_flat_keys=["primary_action"], + ) + self.assertIn("flat_keys=", line) + self.assertNotIn("flat_unchanged=1", line) - def test_apply_pr_merge_status_pending_watch_cmd(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "number": 308, - "url": "https://example.com/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pending_checks": ["build"], - "pr_merge_ready": False, - }, - ): - mod._apply_pr_merge_status(status) - self.assertIn("gh pr checks 308 --watch", status["merge_hint"]) + def test_lfg_flat_field_keys_present_stderr(self) -> None: + keys = mod._lfg_flat_field_keys_present_stderr( + { + "lfg_flat_field_keys_present": [ + "primary_action", + "fc_run_id", + ], + } + ) + self.assertEqual(keys, ["primary_action", "fc_run_id"]) + keys = mod._lfg_flat_field_keys_present_stderr( + { + "primary_action": "gate_watch", + "fc_run_id": 2, + } + ) + self.assertEqual(keys, ["primary_action", "fc_run_id"]) - def test_apply_pr_merge_status_conflicts_hint(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "url": "https://example.com/pr/308", - "lfg_merge_blocked": "pr_merge_conflicts", - "pr_merge_ready": False, - }, - ): - mod._apply_pr_merge_status(status) - self.assertIn("merge conflicts", status["merge_hint"]) + def test_lfg_briefing_mirror_stderr_parts_flat_keys(self) -> None: + joined = " ".join( + mod._lfg_briefing_mirror_stderr_parts( + { + "lfg_flat_field_keys_present": [ + "primary_action", + "fc_run_id", + "watch_recommended", + ], + "primary_action": "gate_watch", + "fc_run_id": 2, + "watch_recommended": True, + } + ) + ) + self.assertIn("flat_fields=3", joined) + self.assertIn( + "flat_keys=primary_action,fc_run_id,watch_recommended", + joined, + ) - def test_compute_lfg_exit_code_pr_pending(self) -> None: - code = mod._compute_lfg_exit_code( + def test_build_lfg_flat_field_keys_present_order(self) -> None: + present = mod._build_lfg_flat_field_keys_present( { - "gh_ok": True, - "lfg_track_complete": True, - "pr_merge_status": {"ok": True, "pr_merge_ready": False}, - }, - deferred=False, - strict_defer_exit=False, - strict_pr_ci_exit=True, - dispatch_on_proceed=False, - execute=False, - sync_docs_after_dispatch=False, - write=False, - lfg_refresh=False, + "fc_run_id": 2, + "primary_action": "gate_watch", + "verify_run_id": 1, + } + ) + self.assertEqual( + present, + ["primary_action", "verify_run_id", "fc_run_id"], ) - self.assertEqual(code, 3) - def test_compute_lfg_exit_reason_pr_pending(self) -> None: - reason = mod._compute_lfg_exit_reason( - { - "lfg_merge_blocked": "pr_checks_pending", - "pr_merge_status": {"lfg_merge_blocked": "pr_checks_pending"}, + def test_apply_lfg_agent_briefing_sets_flat_field_keys_present(self) -> None: + status: dict[str, Any] = { + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], }, - 3, - deferred=False, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + mod._apply_lfg_agent_briefing(status) + present = status.get("lfg_flat_field_keys_present") or [] + self.assertIn("wait_recommended", present) + self.assertIn("ci_drift", present) + self.assertIn("fc_run_id", present) + flat_values = status.get("lfg_flat_field_values") or {} + self.assertEqual(present, mod._build_lfg_flat_field_keys_present(flat_values)) + + def test_mirror_preflight_watch_summary_flat_field_keys_present(self) -> None: + summary: dict[str, Any] = {"polls": 1} + status: dict[str, Any] = { + "primary_action": "gate_watch", + "verify_run_id": 10, + "watch_recommended": True, + } + mod._mirror_preflight_watch_summary_from_status(status, summary) + present = summary.get("lfg_flat_field_keys_present") or [] + self.assertEqual( + present, + ["primary_action", "watch_recommended", "verify_run_id"], ) - self.assertEqual(reason, "pr_checks_pending") - def test_compute_lfg_exit_reason_pending_watch_queue(self) -> None: - reason = mod._compute_lfg_exit_reason( - { - "lfg_merge_blocked": "pr_checks_pending", - "pr_ci_recommendation": { - "action": "watch_queue", - "reason": "runner queue backlog", - "command": "watch-cmd", - }, - }, - 3, - deferred=False, + def test_should_attach_lfg_mirror_stderr_flat_fields_only(self) -> None: + self.assertTrue( + mod._should_attach_lfg_mirror_stderr( + { + "primary_action": "gate_watch", + "fc_run_id": 1, + } + ) ) - self.assertEqual(reason, "pr_checks_pending:watch_queue") + self.assertFalse(mod._should_attach_lfg_mirror_stderr({})) - def test_compute_lfg_exit_reason_pending_defer_external(self) -> None: - reason = mod._compute_lfg_exit_reason( - { - "lfg_merge_blocked": "pr_checks_pending", - "pr_ci_recommendation": {"action": "defer_external"}, + def test_emit_lfg_strict_exit_stderr_top_level_flat_only(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:fc_active_pending", + "briefing_action": "defer", + "primary_action": "gate_watch", + "fc_run_id": 26549293445, + "lfg_flat_field_values": { + "briefing_action": "defer", + "primary_action": "gate_watch", + "fc_run_id": 26549293445, }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + output = err.getvalue() + self.assertIn("flat_fields=3", output) + self.assertIn("primary_action=gate_watch", output) + self.assertIn("fc_run=26549293445", output) + + def test_lfg_flat_field_stderr_count(self) -> None: + self.assertEqual( + mod._lfg_flat_field_stderr_count( + { + "lfg_flat_field_values": { + "primary_action": "gate_watch", + "verify_run_id": 1, + "watch_recommended": True, + }, + } + ), 3, - deferred=False, ) - self.assertEqual(reason, "pr_checks_pending:defer_external") + self.assertEqual( + mod._lfg_flat_field_stderr_count( + { + "primary_action": "gate_watch", + "verify_run_id": 99, + } + ), + 2, + ) - def test_compute_lfg_exit_reason_failed_fix_checks(self) -> None: - reason = mod._compute_lfg_exit_reason( - { - "lfg_merge_blocked": "pr_checks_failed", - "pr_ci_recommendation": {"action": "fix_checks"}, - }, - 3, - deferred=False, + def test_lfg_briefing_mirror_stderr_parts_flat_fields(self) -> None: + joined = " ".join( + mod._lfg_briefing_mirror_stderr_parts( + { + "primary_action": "gate_watch", + "fc_run_id": 2, + "watch_recommended": True, + "lfg_flat_field_values": { + "primary_action": "gate_watch", + "fc_run_id": 2, + "watch_recommended": True, + }, + } + ) ) - self.assertEqual(reason, "pr_checks_failed:fix_checks") + self.assertIn("flat_fields=3", joined) + self.assertIn("primary_action=gate_watch", joined) - def test_emit_lfg_strict_exit_stderr(self) -> None: + def test_emit_lfg_strict_exit_stderr_flat_fields(self) -> None: status: dict[str, Any] = { - "lfg_exit_reason": "pr_checks_pending:watch_queue", - "pr_ci_recommendation": {"command": "watch-cmd"}, + "lfg_exit_reason": "deferred:fc_active_pending", + "briefing_action": "defer", + "primary_action": "gate_watch", + "fc_run_id": 26549293445, + "lfg_agent_briefing": {"action": "defer"}, + "lfg_flat_field_values": { + "briefing_action": "defer", + "primary_action": "gate_watch", + "fc_run_id": 26549293445, + }, } with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: - mod._emit_lfg_strict_exit_stderr(status, 3) - self.assertIn("code=3", err.getvalue()) - self.assertIn("watch_queue", err.getvalue()) - self.assertIn("watch-cmd", err.getvalue()) - - def test_watch_pr_merge_status_conflicts(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "url": "https://example.com/pr/308", - "lfg_merge_blocked": "pr_merge_conflicts", - "pr_merge_ready": False, - }, - ): - mod._watch_pr_merge_status( - status, interval_sec=0.0, timeout_sec=60.0, stall_polls=99 - ) - self.assertEqual(status["lfg_pr_watch_result"], "pr_merge_conflicts") + mod._emit_lfg_strict_exit_stderr(status, 2) + self.assertIn("flat_fields=3", err.getvalue()) - def test_check_detail_record_started_at(self) -> None: - detail = mod._check_detail_record( + def test_build_lfg_flat_field_values_omits_empty(self) -> None: + values = mod._build_lfg_flat_field_values( { - "name": "build", - "startedAt": "2026-05-24T12:00:00Z", - "detailsUrl": "https://example.com/job/1", - "workflowName": "CI", + "primary_action": "gate_watch", + "briefing_action": "", + "active_runs": [], + "watch_recommended": True, + "briefing_merge_ready": False, + "verify_run_id": 99, } ) - self.assertEqual(detail["started_at"], "2026-05-24T12:00:00Z") - empty = mod._check_detail_record({"name": "queued", "startedAt": "0001-01-01T00:00:00Z"}) - self.assertEqual(empty["started_at"], "") - - def test_build_pr_ci_bottlenecks_sorted(self) -> None: - pr_status = { - "in_progress_check_details": [ - {"name": "new", "started_at": "2026-05-24T13:00:00Z", "workflow": "CI"}, - {"name": "old", "started_at": "2026-05-24T12:00:00Z", "workflow": "CI"}, - ], - "pending_check_details": [ - {"name": "queued", "started_at": "", "workflow": "CI"}, - ], - } - bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) - self.assertEqual(bottlenecks["in_progress"][0]["name"], "old") - self.assertEqual(bottlenecks["queued_longest_wait"][0]["name"], "queued") - self.assertFalse(bottlenecks["queue_backlog"]) - - def test_build_pr_ci_bottlenecks_queue_backlog(self) -> None: - pr_status = { - "checks_pending": 5, - "checks_in_progress": 0, - "in_progress_check_details": [], - "pending_check_details": [{"name": "label", "started_at": "", "workflow": "CI"}], - } - bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) - self.assertTrue(bottlenecks["queue_backlog"]) - self.assertIsNone(bottlenecks["oldest_queued_age_hours"]) - - def test_oldest_started_at_hours(self) -> None: - details = [ - {"started_at": "2026-05-27T20:00:00Z"}, - {"started_at": "2026-05-27T18:00:00Z"}, - ] - oldest_at, hours = mod._oldest_started_at_hours(details) - self.assertEqual(oldest_at, "2026-05-27T18:00:00Z") - self.assertIsNotNone(hours) - - def test_build_pr_ci_bottlenecks_oldest_age(self) -> None: - pr_status = { - "checks_pending": 2, - "checks_in_progress": 0, - "in_progress_check_details": [], - "pending_check_details": [ - {"name": "new", "started_at": "2026-05-27T22:00:00Z", "workflow": "CI"}, - {"name": "old", "started_at": "2026-05-27T20:00:00Z", "workflow": "CI"}, - ], - } - bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) - self.assertEqual(bottlenecks["oldest_queued_started_at"], "2026-05-27T20:00:00Z") - self.assertIsNotNone(bottlenecks["oldest_queued_age_hours"]) - - def test_fetch_pr_checks_crosscheck(self) -> None: - payload = [{"name": "build", "state": "QUEUED"}, {"name": "lint", "state": "SUCCESS"}] - with patch.object(mod.subprocess, "run") as mock_run: - mock_run.return_value = subprocess.CompletedProcess( - args=["gh", "pr", "checks"], - returncode=0, - stdout=json.dumps(payload), - stderr="", - ) - cross = mod._fetch_pr_checks_crosscheck(308, 27) - self.assertTrue(cross["ok"]) - self.assertEqual(cross["gh_checks_total"], 2) - self.assertEqual(cross["rollup_vs_gh_delta"], 25) - self.assertEqual(cross["gh_state_counts"]["QUEUED"], 1) - - def test_build_pr_checks_crosscheck_note(self) -> None: - note = mod._build_pr_checks_crosscheck_note( - { - "ok": True, - "rollup_checks_total": 28, - "gh_checks_total": 26, - "rollup_vs_gh_delta": 2, - "gh_state_counts": {"QUEUED": 25, "SKIPPED": 1}, - }, - queue_backlog=True, - ) - self.assertIn("delta +2", note) - self.assertIn("gh reports 25 QUEUED", note) - self.assertEqual( - mod._build_pr_checks_crosscheck_note( - {"ok": True, "rollup_vs_gh_delta": 0}, - queue_backlog=False, - ), - "", - ) - - def test_emit_lfg_strict_exit_stderr_crosscheck(self) -> None: - status: dict[str, Any] = { - "lfg_exit_reason": "pr_checks_pending:watch_queue", - "pr_ci_recommendation": {"command": "watch-cmd"}, - "pr_checks_crosscheck_note": "delta +2", - } - with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: - mod._emit_lfg_strict_exit_stderr(status, 3) - output = err.getvalue() - self.assertIn("crosscheck=delta +2", output) - - def test_build_lfg_agent_briefing_watch_queue(self) -> None: + self.assertEqual(values.get("primary_action"), "gate_watch") + self.assertTrue(values.get("watch_recommended")) + self.assertFalse(values.get("briefing_merge_ready")) + self.assertEqual(values.get("verify_run_id"), 99) + self.assertNotIn("briefing_action", values) + self.assertNotIn("active_runs", values) + + def test_apply_lfg_agent_briefing_sets_flat_field_values(self) -> None: status: dict[str, Any] = { - "lfg_track_complete": True, - "lfg_merge_blocked": "pr_checks_pending", - "lfg_exit_code": 3, - "lfg_exit_reason": "pr_checks_pending:watch_queue", - "pr_queue_backlog_note": "runner backlog", - "pr_checks_crosscheck_note": "delta +2", - "pr_ci_recommendation": { - "action": "watch_queue", - "reason": "runner queue backlog", - "command": "watch-cmd", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], }, - "pr_merge_status": { - "ok": True, - "number": 308, - "url": "https://example.com/pr/308", - "pr_merge_ready": False, - "checks_pending": 27, - "checks_in_progress": 0, - "pr_ci_progress": {"completion_percent": 4}, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", }, } - briefing = mod._build_lfg_agent_briefing(status) - self.assertEqual(briefing["action"], "watch_queue") - self.assertEqual(briefing["exit_code"], 3) - self.assertEqual(len(briefing["notes"]), 2) - self.assertEqual(briefing["completion_percent"], 4) - - def test_build_lfg_agent_briefing_no_pr(self) -> None: + mod._apply_lfg_agent_briefing(status) + flat_values = status.get("lfg_flat_field_values") or {} + self.assertTrue(flat_values.get("wait_recommended")) + self.assertIn("fields", flat_values.get("ci_drift") or {}) + self.assertEqual(flat_values.get("fc_run_id"), 2) + self.assertNotIn("sha_gap", flat_values) + + def test_mirror_preflight_watch_summary_flat_field_values(self) -> None: + summary: dict[str, Any] = {"polls": 1} status: dict[str, Any] = { - "lfg_track_complete": True, - "lfg_merge_blocked": "no_open_pr", - "pr_merge_status": {"ok": False}, - "pr_ci_recommendation": { - "action": "no_pr", - "reason": "no open PR on branch", - "command": "", - }, + "primary_action": "gate_watch", + "verify_run_id": 10, + "watch_recommended": True, + "lfg_flat_field_keys": list(mod.LFG_FLAT_FIELD_KEYS), } - briefing = mod._build_lfg_agent_briefing(status) - self.assertEqual(briefing["action"], "no_pr") - self.assertEqual(briefing["blocked"], "no_open_pr") - - def test_build_lfg_agent_briefing_merge_ready(self) -> None: + mod._mirror_preflight_watch_summary_from_status(status, summary) + flat_values = summary.get("lfg_flat_field_values") or {} + self.assertEqual(flat_values.get("primary_action"), "gate_watch") + self.assertEqual(flat_values.get("verify_run_id"), 10) + self.assertTrue(flat_values.get("watch_recommended")) + + def test_lfg_flat_field_keys_constant(self) -> None: + self.assertIn("verify_run_id", mod.LFG_FLAT_FIELD_KEYS) + self.assertIn("wait_recommended", mod.LFG_FLAT_FIELD_KEYS) + self.assertIn("ci_drift", mod.LFG_FLAT_FIELD_KEYS) + + def test_apply_lfg_agent_briefing_sets_flat_field_keys(self) -> None: status: dict[str, Any] = { - "lfg_track_complete": True, - "lfg_exit_code": 0, - "lfg_exit_reason": "merge_ready", - "pr_ci_recommendation": { - "action": "merge", - "reason": "PR CI complete", - "command": "gh pr merge 308 --squash --auto", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [{"field": "forward_commits_run_id", "doc": 1, "live": 2}], }, - "pr_merge_status": { - "ok": True, - "number": 308, - "url": "https://example.com/pr/308", - "pr_merge_ready": True, - "pr_ci_progress": {"completion_percent": 100}, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", }, } - briefing = mod._build_lfg_agent_briefing(status) - self.assertEqual(briefing["action"], "merge") - self.assertTrue(briefing["merge_ready"]) - self.assertIn("gh pr merge", briefing["command"]) + mod._apply_lfg_agent_briefing(status) + self.assertEqual(status.get("lfg_flat_field_keys"), list(mod.LFG_FLAT_FIELD_KEYS)) - def test_build_lfg_agent_briefing_blocked_refresh(self) -> None: + def test_mirror_preflight_watch_summary_flat_field_keys(self) -> None: + summary: dict[str, Any] = {"polls": 1} status: dict[str, Any] = { - "lfg_refresh_blocked": "classify_fc_stale_gap", - "proceed_hint": ( - "python3 .github/scripts/local_verify_pypi_slice.py " - "--prefetch-git --lfg-gate" - ), + "lfg_flat_field_keys": list(mod.LFG_FLAT_FIELD_KEYS), + "primary_action": "gate_watch", } - briefing = mod._build_lfg_agent_briefing(status) - self.assertEqual(briefing["action"], "blocked_refresh") - self.assertIn("--prefetch-git", briefing["command"]) - self.assertEqual(briefing["reason"], "classify_fc_stale_gap") - - def test_should_emit_lfg_agent_briefing_stderr(self) -> None: - self.assertTrue( - mod._should_emit_lfg_agent_briefing_stderr( - {"action": "blocked_refresh"}, - 0, - ) + mod._mirror_preflight_watch_summary_from_status(status, summary) + self.assertEqual(summary.get("lfg_flat_field_keys"), list(mod.LFG_FLAT_FIELD_KEYS)) + self.assertEqual(summary.get("primary_action"), "gate_watch") + + def test_mirror_lfg_flat_fields_from_briefing(self) -> None: + target: dict[str, Any] = {"existing": True} + briefing: dict[str, Any] = { + "action": "investigate_ci_drift", + "reason": "fc_active_pending", + "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + "active_runs": ["fc"], + "verify_run_id": 1, + "fc_run_id": 2, + "wait_recommended": True, + "drift": {"fields": [{"field": "forward_commits_run_id"}]}, + "monitor_commands": { + "watch_fc_run": "gh run watch 2 --exit-status", + }, + } + mod._mirror_lfg_flat_fields(briefing, target, clear_missing=True) + self.assertEqual(target.get("briefing_action"), "investigate_ci_drift") + self.assertEqual(target.get("briefing_reason"), "fc_active_pending") + self.assertIn("--lfg-gate-watch", target.get("briefing_command") or "") + self.assertEqual(target.get("active_runs"), ["fc"]) + self.assertEqual(target.get("verify_run_id"), 1) + self.assertEqual(target.get("fc_run_id"), 2) + self.assertTrue(target.get("wait_recommended")) + self.assertEqual( + (target.get("ci_drift") or {}).get("fields"), + [{"field": "forward_commits_run_id"}], ) - self.assertFalse( - mod._should_emit_lfg_agent_briefing_stderr( - {"action": "merge"}, - 0, - ) + self.assertEqual( + target.get("gh_watch_command"), + "gh run watch 2 --exit-status", ) - self.assertTrue( - mod._should_emit_lfg_agent_briefing_stderr( - {"action": "investigate_ci_drift"}, - 0, - ) + + def test_dedupe_preserve_order(self) -> None: + self.assertEqual( + mod._dedupe_preserve_order(["a", "b", "a", "c", "b"]), + ["a", "b", "c"], ) - def test_build_lfg_agent_briefing_investigate_drift(self) -> None: - status: dict[str, Any] = { - "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-refresh --dry-run", - "checkpoint": { - "proceed_reason": "investigate_ci_drift", - "ci_drift_note": "FC run 26543899770 vs doc 26365648344", - }, - } - briefing = mod._build_lfg_agent_briefing(status) - self.assertEqual(briefing["action"], "investigate_ci_drift") - self.assertIn("26543899770", briefing["notes"][0]) + def test_summarize_pr_checks_dedupes_pending_names(self) -> None: + summary = mod._summarize_pr_checks( + [ + {"name": "Analyze (python)", "conclusion": "", "status": "QUEUED"}, + {"name": "Analyze (python)", "conclusion": "", "status": "IN_PROGRESS"}, + {"name": "build", "conclusion": "", "status": "QUEUED"}, + ] + ) + self.assertEqual(summary["pending_checks"], ["Analyze (python)", "build"]) + self.assertEqual(summary["checks_pending"], 3) - def test_emit_lfg_agent_briefing_stderr(self) -> None: - with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: - mod._emit_lfg_agent_briefing_stderr( + def test_summarize_pr_checks_in_progress_and_details(self) -> None: + summary = mod._summarize_pr_checks( + [ { - "action": "watch_queue", - "exit_code": 3, - "blocked": "pr_checks_pending", - "completion_percent": 4, - } - ) - output = err.getvalue() - self.assertIn("LFG briefing:", output) - self.assertIn("action=watch_queue", output) - self.assertIn("complete=4%", output) - - def test_apply_pr_merge_status_queue_backlog_hint(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", - return_value={ - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "checks_pending": 26, - "checks_in_progress": 0, - "checks_total": 27, - "pending_checks": ["label"], - "pending_check_details": [ - { - "name": "label", - "started_at": "2026-05-27T21:30:00Z", - "workflow": "CI", - "details_url": "", - }, - ], - "pr_merge_ready": False, - }, - ): - with patch.object( - mod, - "_fetch_pr_checks_crosscheck", - return_value={ - "ok": True, - "gh_checks_total": 25, - "rollup_checks_total": 27, - "rollup_vs_gh_delta": 2, - "gh_state_counts": {"QUEUED": 24}, + "name": "build", + "conclusion": "", + "status": "IN_PROGRESS", + "detailsUrl": "https://example.com/job/1", + "workflowName": "CI", }, - ): - mod._apply_pr_merge_status(status) - self.assertIn("runner backlog", status["merge_hint"]) - self.assertIn("oldest ~", status["merge_hint"]) - self.assertIn("pr_checks_crosscheck", status) - rec = status.get("pr_ci_recommendation") or {} - self.assertEqual(rec.get("action"), "watch_queue") - self.assertIn("pr_queue_backlog_note", status) - self.assertIn("pr_checks_crosscheck_note", status) - self.assertIn("delta +2", status["merge_hint"]) - self.assertIn("gh reports 24 QUEUED", status["pr_checks_crosscheck_note"]) + { + "name": "build", + "conclusion": "", + "status": "QUEUED", + "detailsUrl": "https://example.com/job/2", + "workflowName": "CI", + }, + { + "name": "lint", + "conclusion": "FAILURE", + "status": "COMPLETED", + "detailsUrl": "https://example.com/job/3", + "workflowName": "Lint", + }, + ] + ) + self.assertEqual(summary["checks_in_progress"], 1) + self.assertEqual(summary["checks_queued"], 1) + self.assertEqual(summary["checks_pending"], 2) + self.assertEqual(len(summary["pending_check_details"]), 1) + self.assertEqual(len(summary["in_progress_check_details"]), 1) + self.assertEqual(summary["in_progress_check_details"][0]["details_url"], "https://example.com/job/1") + self.assertEqual(len(summary["failed_check_details"]), 1) + self.assertEqual(summary["failed_check_details"][0]["workflow"], "Lint") - def test_pr_ci_recommendation_merge_ready(self) -> None: - status: dict[str, Any] = { - "pr_merge_status": {"ok": True, "pr_merge_ready": True}, - "merge_actions": {"merge_squash_auto": "gh pr merge 308 --squash --auto"}, - } - rec = mod._build_pr_ci_recommendation(status) - self.assertEqual(rec["action"], "merge") - self.assertIn("gh pr merge", rec["command"]) + def test_summarize_pr_checks_ci_progress(self) -> None: + summary = mod._summarize_pr_checks( + [ + {"name": "a", "conclusion": "SUCCESS", "status": "COMPLETED"}, + {"name": "b", "conclusion": "SKIPPED", "status": "COMPLETED"}, + {"name": "c", "conclusion": "", "status": "QUEUED"}, + {"name": "d", "conclusion": "FAILURE", "status": "COMPLETED"}, + ] + ) + progress = summary["pr_ci_progress"] + self.assertEqual(progress["total"], 4) + self.assertEqual(progress["terminal"], 3) + self.assertEqual(progress["remaining"], 1) + self.assertEqual(progress["completion_percent"], 75) - def test_pr_ci_recommendation_defer_severe(self) -> None: - status: dict[str, Any] = { - "pr_merge_status": {"ok": True, "lfg_merge_blocked": "pr_checks_pending"}, - "lfg_merge_blocked": "pr_checks_pending", - "pr_ci_bottlenecks": { - "queue_backlog": True, - "queue_backlog_severe": True, - "oldest_queued_age_hours": 5.0, - }, - "merge_actions": {}, - } - rec = mod._build_pr_ci_recommendation(status) - self.assertEqual(rec["action"], "defer_external") + def test_summarize_pr_checks_status_context(self) -> None: + summary = mod._summarize_pr_checks( + [ + { + "context": "ci/circleci", + "state": "SUCCESS", + "targetUrl": "https://example.com/status/1", + }, + { + "context": "ci/travis", + "state": "PENDING", + "targetUrl": "https://example.com/status/2", + }, + ] + ) + self.assertEqual(summary["checks_success"], 1) + self.assertEqual(summary["checks_pending"], 1) + self.assertIn("ci/travis", summary["pending_checks"]) + self.assertFalse(summary["pr_merge_ready"]) - def test_build_pr_queue_backlog_note(self) -> None: - note = mod._build_pr_queue_backlog_note( + def test_check_detail_record_uses_context(self) -> None: + detail = mod._check_detail_record( + {"context": "ci/travis", "targetUrl": "https://example.com/t", "state": "PENDING"} + ) + self.assertEqual(detail["name"], "ci/travis") + self.assertEqual(detail["details_url"], "https://example.com/t") + + def test_format_watch_poll_line_includes_percent(self) -> None: + line = mod._format_watch_poll_line( { - "queue_backlog": True, - "oldest_queued_age_hours": 5.0, - "queue_backlog_severe": True, + "checks_pending": 2, + "checks_in_progress": 1, + "checks_failed": 0, + "checks_success": 5, + "pr_ci_progress": {"completion_percent": 62}, } ) - self.assertIn("severe", note) - self.assertIn("5.0h", note) + self.assertIn("complete=62%", line) + self.assertIn("skipped=", line) - def test_queue_backlog_severe(self) -> None: - pr_status = { - "checks_pending": 5, + def test_watch_snapshot_progress_key_and_compact_line(self) -> None: + snapshot = { + "completion_percent": 4, + "checks_pending": 27, "checks_in_progress": 0, - "in_progress_check_details": [], - "pending_check_details": [ - {"name": "old", "started_at": "2026-05-27T10:00:00Z", "workflow": "CI"}, - ], - } - with patch.object(mod, "_hours_since_iso", return_value=5.0): - bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) - self.assertTrue(bottlenecks["queue_backlog_severe"]) - - def test_queue_stall_event_dedupe_by_pending(self) -> None: - events = [{"poll": 1, "checks_pending": 26, "hint": "backlog"}] - last_pending = events[-1].get("checks_pending") - self.assertFalse(last_pending != 26) - self.assertTrue(last_pending != 25) - - def test_build_pr_watch_summary_includes_crosscheck(self) -> None: - status: dict[str, Any] = { - "lfg_pr_watch_result": "ready", - "pr_watch_history": [ - {"completion_percent": 4, "checks_pending": 26}, - {"completion_percent": 100, "checks_pending": 0}, - ], - "pr_queue_stall_events": [], - "pr_watch_started_monotonic": mod.time.monotonic() - 10.0, - "pr_ci_bottlenecks": {"oldest_queued_age_hours": 0.5, "queue_backlog_severe": False}, - "pr_checks_crosscheck": {"rollup_vs_gh_delta": 2}, + "checks_success": 1, + "checks_failed": 0, } - summary = mod._build_pr_watch_summary(status) - self.assertEqual(summary.get("rollup_vs_gh_delta"), 2) - self.assertFalse(summary.get("queue_backlog_severe")) - - def test_evaluate_pr_watch_stall_queue(self) -> None: - recent = [ - { - "completion_percent": 4, - "checks_pending": 26, - "checks_in_progress": 0, - } - for _ in range(3) - ] - stall = mod._evaluate_pr_watch_stall( - recent, - stall_polls=3, - interval_sec=30.0, - bottlenecks={ - "in_progress": [], - "queued_longest_wait": [{"name": "label"}], - }, - next_name="label", + self.assertEqual( + mod._watch_snapshot_progress_key(snapshot), + (4, 27, 0, 1, 0), ) - self.assertIsNotNone(stall) - assert stall is not None - self.assertEqual(stall["lfg_pr_watch_result"], "queue_stalled") - self.assertEqual(stall["lfg_merge_blocked"], "pr_queue_stalled") - self.assertIn("queue backlog", stall["merge_hint"]) + self.assertIn("unchanged complete=4%", mod._format_compact_watch_poll_line(snapshot)) - def test_evaluate_pr_watch_stall_job_hang(self) -> None: - recent = [ - { - "completion_percent": 42, - "checks_pending": 10, - "checks_in_progress": 2, - } - for _ in range(3) + def test_count_unchanged_watch_polls(self) -> None: + history = [ + {"completion_percent": 4, "checks_pending": 27, "checks_in_progress": 0, "checks_success": 1, "checks_failed": 0}, + {"completion_percent": 4, "checks_pending": 27, "checks_in_progress": 0, "checks_success": 1, "checks_failed": 0}, + {"completion_percent": 8, "checks_pending": 25, "checks_in_progress": 1, "checks_success": 2, "checks_failed": 0}, + {"completion_percent": 8, "checks_pending": 25, "checks_in_progress": 1, "checks_success": 2, "checks_failed": 0}, ] - stall = mod._evaluate_pr_watch_stall( - recent, - stall_polls=3, - interval_sec=30.0, - bottlenecks={ - "in_progress": [{"name": "CodeQL", "workflow": "Analyze"}], - "queued_longest_wait": [], - }, - next_name="CodeQL", - ) - self.assertIsNotNone(stall) - assert stall is not None - self.assertEqual(stall["lfg_pr_watch_result"], "stalled") - self.assertIn("job hang", stall["merge_hint"]) + self.assertEqual(mod._count_unchanged_watch_polls(history), 2) - def test_resolve_merge_watch_default_timeout(self) -> None: - self.assertEqual( - mod._resolve_watch_timeout_seconds(None, lfg_merge_watch=True), - 7200.0, + def test_should_emit_watch_heartbeat(self) -> None: + self.assertFalse( + mod._should_emit_watch_heartbeat(True, 11, 12), ) - self.assertEqual( - mod._resolve_watch_timeout_seconds(None, lfg_merge_watch=False), - 1800.0, + self.assertTrue( + mod._should_emit_watch_heartbeat(True, 12, 12), ) - self.assertEqual( - mod._resolve_watch_timeout_seconds(900.0, lfg_merge_watch=True), - 900.0, + self.assertFalse( + mod._should_emit_watch_heartbeat(True, 12, 0), ) - def test_build_pr_watch_summary(self) -> None: - status: dict[str, Any] = { - "lfg_pr_watch_result": "ready", - "pr_watch_history": [ + def test_watch_pr_merge_status_heartbeat_poll(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + pending_status = { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "checks_pending": 27, + "checks_in_progress": 0, + "checks_success": 1, + "checks_failed": 0, + "checks_skipped": 0, + "pr_ci_progress": {"completion_percent": 4, "remaining": 27, "total": 28}, + "pending_check_details": [ { - "completion_percent": 4, - "checks_pending": 26, - "checks_queued": 26, - }, - { - "completion_percent": 100, - "checks_pending": 0, - "checks_queued": 0, + "name": "label", + "started_at": "2026-05-27T21:30:00Z", + "workflow": "CI", + "details_url": "", }, ], - "pr_queue_stall_events": [{"poll": 1, "hint": "backlog"}], - "pr_watch_started_monotonic": mod.time.monotonic() - 60.0, - } - summary = mod._build_pr_watch_summary(status) - self.assertEqual(summary["completion_percent_delta"], 96) - self.assertEqual(summary["checks_pending_delta"], -26) - self.assertEqual(summary["queue_stall_events"], 1) - self.assertEqual(summary["lfg_pr_watch_result"], "ready") - - def test_watch_pr_merge_status_queue_stall_exits_when_flagged(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - queue_progress = { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pr_merge_ready": False, - "checks_pending": 26, - "checks_in_progress": 0, - "pr_ci_progress": {"completion_percent": 4}, - "in_progress_check_details": [], - "pending_check_details": [ - {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, - ], - } - - with patch.object(mod, "_fetch_pr_merge_status", return_value=queue_progress): - with patch.object(mod.time, "sleep"): - with patch("sys.stderr", new_callable=io.StringIO) as err: - mod._watch_pr_merge_status( - status, - interval_sec=30.0, - timeout_sec=3600.0, - stall_polls=3, - exit_on_queue_stall=True, - ) - self.assertEqual(status["lfg_pr_watch_result"], "queue_stalled") - self.assertTrue(status["pr_queue_stalled"]) - self.assertIn("queue_backlog=label", err.getvalue()) - - def test_watch_pr_merge_status_continues_through_queue_stall(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - queue_progress = { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", "pr_merge_ready": False, - "checks_pending": 26, - "checks_in_progress": 0, - "pr_ci_progress": {"completion_percent": 4}, - "in_progress_check_details": [], - "pending_check_details": [ - {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, - ], } calls = {"n": 0} def fetch_side() -> dict[str, Any]: calls["n"] += 1 - if calls["n"] <= 3: - return queue_progress - return { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "pr_merge_ready": True, - "lfg_merge_blocked": None, - } + if calls["n"] >= 14: + return { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "pr_merge_ready": True, + "lfg_merge_blocked": None, + } + return dict(pending_status) with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): - with patch.object(mod.time, "sleep"): - with patch("sys.stderr", new_callable=io.StringIO) as err: - mod._watch_pr_merge_status( - status, - interval_sec=30.0, - timeout_sec=3600.0, - stall_polls=3, - ) - self.assertEqual(status["lfg_pr_watch_result"], "ready") - self.assertTrue(status["pr_queue_stalled"]) - self.assertEqual(len(status["pr_queue_stall_events"]), 1) - self.assertIn("continuing watch", err.getvalue()) + with patch.object( + mod, + "_fetch_pr_checks_crosscheck", + return_value={ + "ok": True, + "gh_checks_total": 26, + "rollup_checks_total": 28, + "rollup_vs_gh_delta": 2, + "gh_state_counts": {"QUEUED": 25}, + }, + ): + with patch.object(mod.time, "sleep"): + with patch("sys.stderr", new_callable=io.StringIO) as err: + mod._watch_pr_merge_status( + status, + interval_sec=0.0, + timeout_sec=60.0, + stall_polls=99, + heartbeat_polls=12, + ) + output = err.getvalue() + self.assertIn("PR watch poll 13:", output) + poll13 = output.split("PR watch poll 13:")[1].split("\n")[0] + self.assertIn("heartbeat=1", poll13) + self.assertIn("success=", poll13) + self.assertIn("rollup_delta=", poll13) + summary = status.get("pr_watch_summary") or {} + self.assertEqual(summary.get("heartbeat_polls"), 1) - def test_watch_pr_merge_status_queue_timeout(self) -> None: + def test_watch_pr_merge_status_compact_unchanged_polls(self) -> None: status: dict[str, Any] = {"lfg_track_complete": True} - queue_progress = { + pending_status = { "ok": True, "number": 308, "url": "https://github.com/example/pr/308", "lfg_merge_blocked": "pr_checks_pending", - "pr_merge_ready": False, - "checks_pending": 26, + "checks_pending": 27, "checks_in_progress": 0, - "pr_ci_progress": {"completion_percent": 4}, - "in_progress_check_details": [], + "checks_success": 1, + "checks_failed": 0, + "checks_skipped": 0, + "pr_ci_progress": {"completion_percent": 4, "remaining": 27, "total": 28}, "pending_check_details": [ - {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, - ], - } - with patch.object(mod, "_fetch_pr_merge_status", return_value=queue_progress): - with patch.object(mod.time, "sleep"): - with patch.object(mod.time, "monotonic", side_effect=[0.0, 0.0, 1.0, 1.0]): - mod._watch_pr_merge_status( - status, - interval_sec=30.0, - timeout_sec=0.0, - stall_polls=99, - ) - self.assertEqual(status["lfg_pr_watch_result"], "queue_timeout") - self.assertEqual(status["lfg_merge_blocked"], "pr_queue_stalled") - self.assertIn("queue backlog", status["merge_hint"]) - - def test_watch_pr_merge_status_stalled(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - stalled_progress = { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pr_merge_ready": False, - "checks_pending": 10, - "checks_in_progress": 2, - "pr_ci_progress": {"completion_percent": 42}, - "in_progress_check_details": [ { - "name": "CodeQL", - "started_at": "2026-05-24T10:00:00Z", - "workflow": "Analyze", - "details_url": "https://example.com/1", + "name": "label", + "started_at": "2026-05-27T21:30:00Z", + "workflow": "CI", + "details_url": "", }, ], - "pending_check_details": [], + "pr_merge_ready": False, } + calls = {"n": 0} - with patch.object(mod, "_fetch_pr_merge_status", return_value=stalled_progress): + def fetch_side() -> dict[str, Any]: + calls["n"] += 1 + if calls["n"] >= 3: + return { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "pr_merge_ready": True, + "lfg_merge_blocked": None, + } + return dict(pending_status) + + with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): with patch.object(mod.time, "sleep"): with patch("sys.stderr", new_callable=io.StringIO) as err: mod._watch_pr_merge_status( status, - interval_sec=30.0, - timeout_sec=3600.0, - stall_polls=3, + interval_sec=0.0, + timeout_sec=60.0, + stall_polls=99, ) - self.assertEqual(status["lfg_pr_watch_result"], "stalled") - self.assertTrue(status["pr_watch_stalled"]) - self.assertIn("job hang", status["merge_hint"]) - self.assertEqual(len(status["pr_watch_history"]), 3) - self.assertIn("bottleneck=CodeQL", err.getvalue()) - self.assertIn("PR watch stalled:", err.getvalue()) - - def test_recompare_checkpoint_status(self) -> None: - status: dict[str, Any] = { - "verify_pypi": {"run_id": 1, "status": "completed", "conclusion": "success", "head_sha": "a"}, - "forward_commits": {"run_id": 2, "status": "completed", "conclusion": "success", "head_sha": "b"}, - } - with patch.object(mod, "_compare_checkpoint", return_value={"proceed_reason": "update_monitoring_docs"}): - with patch.object(mod, "_validate_checkpoint_doc", return_value={"doc_valid": True}): - with patch.object(mod, "_refine_lfg_checkpoint") as mock_refine: - mod._recompare_checkpoint_status(status, targets=["solution"]) - self.assertIn("checkpoint", status) - mock_refine.assert_called_once() + output = err.getvalue() + self.assertIn("PR watch poll 2: unchanged", output) + self.assertIn("queue_age=", output) + self.assertNotIn("rollup_delta=", output.split("PR watch poll 2:")[1].split("\n")[0]) + summary = status.get("pr_watch_summary") or {} + self.assertEqual(summary.get("unchanged_polls"), 1) - def test_build_proceed_hint_classify_fc_prefetch(self) -> None: - hint = mod._build_proceed_hint({"checkpoint": {}}, blocked="classify_fc_stale_gap") - self.assertIn("--prefetch-git", hint) - self.assertIn("--lfg-gate", hint) + def test_compute_lfg_exit_code_no_open_pr(self) -> None: + code = mod._compute_lfg_exit_code( + { + "gh_ok": True, + "lfg_track_complete": True, + "lfg_merge_blocked": "no_open_pr", + "pr_merge_status": {"ok": False}, + }, + deferred=False, + strict_defer_exit=False, + strict_pr_ci_exit=True, + dispatch_on_proceed=False, + execute=False, + sync_docs_after_dispatch=False, + write=False, + lfg_refresh=False, + ) + self.assertEqual(code, 3) - def test_apply_pr_merge_status_when_track_complete(self) -> None: + def test_apply_pr_merge_status_no_open_pr(self) -> None: status: dict[str, Any] = {"lfg_track_complete": True} with patch.object( mod, "_fetch_pr_merge_status", - return_value={ - "ok": True, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pending_checks": ["Analyze (python)"], - "pr_merge_ready": False, - }, + return_value={"ok": False, "error": "no open PR"}, ): mod._apply_pr_merge_status(status) - self.assertIn("pr_merge_status", status) - self.assertIn("Analyze (python)", status["merge_hint"]) + self.assertEqual(status["lfg_merge_blocked"], "no_open_pr") - def test_apply_pr_merge_ready_includes_merge_cmd(self) -> None: - status: dict[str, Any] = {"lfg_track_complete": True} - with patch.object( - mod, - "_fetch_pr_merge_status", + def test_build_merge_actions_with_number(self) -> None: + actions = mod._build_merge_actions(308) + self.assertIn("gh pr checks 308 --watch", actions["watch_checks"]) + self.assertIn("gh pr merge 308 --squash --auto", actions["merge_squash_auto"]) + + def test_fetch_pr_merge_status_merged(self) -> None: + payload = { + "number": 308, + "url": "https://example.com/pr/308", + "state": "MERGED", + "mergeable": "UNKNOWN", + "statusCheckRollup": [], + } + with patch.object( + mod.subprocess, + "run", + return_value=mock.Mock(returncode=0, stdout=json.dumps(payload), stderr=""), + ): + result = mod._fetch_pr_merge_status() + self.assertEqual(result["lfg_merge_blocked"], "pr_merged") + self.assertFalse(result["pr_merge_ready"]) + + def test_apply_pr_merge_status_merge_actions_and_next_pending(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", return_value={ "ok": True, "number": 308, - "url": "https://github.com/example/pr/308", - "pr_merge_ready": True, - "lfg_merge_blocked": None, + "url": "https://example.com/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pending_check_details": [ + { + "name": "build", + "details_url": "https://example.com/job/1", + "workflow": "CI", + } + ], + "pr_merge_ready": False, }, ): mod._apply_pr_merge_status(status) - self.assertIn("gh pr merge 308 --squash --auto", status["merge_hint"]) + self.assertIn("watch_checks", status["merge_actions"]) + self.assertEqual(status["next_pending_check"]["name"], "build") - def test_watch_pr_merge_status_ready(self) -> None: + def test_pick_next_pending_check_prefers_in_progress(self) -> None: + picked = mod._pick_next_pending_check( + { + "in_progress_check_details": [ + {"name": "running", "details_url": "https://example.com/r", "workflow": "CI"}, + ], + "pending_check_details": [ + {"name": "queued", "details_url": "https://example.com/q", "workflow": "CI"}, + ], + } + ) + self.assertEqual(picked["name"], "running") + + def test_apply_pr_merge_status_next_failed_check(self) -> None: status: dict[str, Any] = {"lfg_track_complete": True} - calls = {"n": 0} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "number": 308, + "url": "https://example.com/pr/308", + "lfg_merge_blocked": "pr_checks_failed", + "failed_check_details": [ + { + "name": "lint", + "details_url": "https://example.com/job/fail", + "workflow": "Lint", + } + ], + "pr_merge_ready": False, + }, + ): + mod._apply_pr_merge_status(status) + self.assertEqual(status["next_failed_check"]["name"], "lint") + self.assertEqual(status["merge_actions"]["list_failed"], "gh pr checks 308 --failed") - def fetch_side() -> dict[str, Any]: - calls["n"] += 1 - if calls["n"] == 1: - return { - "ok": True, - "number": 308, - "url": "https://github.com/example/pr/308", - "lfg_merge_blocked": "pr_checks_pending", - "pending_checks": ["build"], - "in_progress_check_details": [ - {"name": "build", "details_url": "https://example.com/job/1", "workflow": "CI"}, - ], - "pr_merge_ready": False, - } - return { + def test_compute_lfg_exit_reason_merge_ready(self) -> None: + reason = mod._compute_lfg_exit_reason( + {"pr_merge_status": {"pr_merge_ready": True}}, + 0, + deferred=False, + ) + self.assertEqual(reason, "merge_ready") + + def test_compute_lfg_exit_reason_monitoring_complete(self) -> None: + reason = mod._compute_lfg_exit_reason( + {"lfg_track_complete": True, "pr_merge_status": {"pr_merge_ready": False}}, + 0, + deferred=False, + ) + self.assertEqual(reason, "monitoring_complete") + + def test_summarize_pr_checks_skipped_not_pending(self) -> None: + summary = mod._summarize_pr_checks( + [ + {"name": "label", "conclusion": "SKIPPED", "status": "COMPLETED"}, + {"name": "build", "conclusion": "", "status": "QUEUED"}, + ] + ) + self.assertEqual(summary["checks_skipped"], 1) + self.assertEqual(summary["checks_pending"], 1) + self.assertEqual(summary["pending_checks"], ["build"]) + self.assertFalse(summary["pr_merge_ready"]) + + def test_summarize_pr_checks_merge_ready(self) -> None: + summary = mod._summarize_pr_checks( + [ + {"name": "test", "conclusion": "SUCCESS", "status": "COMPLETED"}, + {"name": "lint", "conclusion": "SKIPPED", "status": "COMPLETED"}, + ] + ) + self.assertTrue(summary["pr_merge_ready"]) + self.assertIsNone(summary["lfg_merge_blocked"]) + + def test_apply_pr_merge_status_failed_names(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ "ok": True, "number": 308, - "url": "https://github.com/example/pr/308", - "pr_merge_ready": True, - "lfg_merge_blocked": None, + "url": "https://example.com/pr/1", + "lfg_merge_blocked": "pr_checks_failed", + "failed_checks": ["Check File Sizes", "devskim"], + "pr_merge_ready": False, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("Check File Sizes", status["merge_hint"]) + self.assertIn("gh pr checks 308 --failed", status["merge_hint"]) + self.assertEqual(status["lfg_merge_blocked"], "pr_checks_failed") + + def test_fetch_pr_merge_status_conflicts(self) -> None: + payload = { + "number": 308, + "url": "https://example.com/pr/308", + "state": "OPEN", + "mergeable": "CONFLICTING", + "statusCheckRollup": [ + {"name": "build", "conclusion": "SUCCESS", "status": "COMPLETED"}, + ], + } + with patch.object( + mod.subprocess, + "run", + return_value=mock.Mock(returncode=0, stdout=json.dumps(payload), stderr=""), + ): + result = mod._fetch_pr_merge_status() + self.assertFalse(result["pr_merge_ready"]) + self.assertEqual(result["lfg_merge_blocked"], "pr_merge_conflicts") + + def test_apply_pr_merge_status_pending_watch_cmd(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "number": 308, + "url": "https://example.com/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pending_checks": ["build"], + "pr_merge_ready": False, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("gh pr checks 308 --watch", status["merge_hint"]) + + def test_apply_pr_merge_status_conflicts_hint(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "url": "https://example.com/pr/308", + "lfg_merge_blocked": "pr_merge_conflicts", + "pr_merge_ready": False, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("merge conflicts", status["merge_hint"]) + + def test_compute_lfg_exit_code_pr_pending(self) -> None: + code = mod._compute_lfg_exit_code( + { + "gh_ok": True, + "lfg_track_complete": True, + "pr_merge_status": {"ok": True, "pr_merge_ready": False}, + }, + deferred=False, + strict_defer_exit=False, + strict_pr_ci_exit=True, + dispatch_on_proceed=False, + execute=False, + sync_docs_after_dispatch=False, + write=False, + lfg_refresh=False, + ) + self.assertEqual(code, 3) + + def test_compute_lfg_exit_reason_pr_pending(self) -> None: + reason = mod._compute_lfg_exit_reason( + { + "lfg_merge_blocked": "pr_checks_pending", + "pr_merge_status": {"lfg_merge_blocked": "pr_checks_pending"}, + }, + 3, + deferred=False, + ) + self.assertEqual(reason, "pr_checks_pending") + + def test_compute_lfg_exit_reason_pending_watch_queue(self) -> None: + reason = mod._compute_lfg_exit_reason( + { + "lfg_merge_blocked": "pr_checks_pending", + "pr_ci_recommendation": { + "action": "watch_queue", + "reason": "runner queue backlog", + "command": "watch-cmd", + }, + }, + 3, + deferred=False, + ) + self.assertEqual(reason, "pr_checks_pending:watch_queue") + + def test_compute_lfg_exit_reason_pending_defer_external(self) -> None: + reason = mod._compute_lfg_exit_reason( + { + "lfg_merge_blocked": "pr_checks_pending", + "pr_ci_recommendation": {"action": "defer_external"}, + }, + 3, + deferred=False, + ) + self.assertEqual(reason, "pr_checks_pending:defer_external") + + def test_compute_lfg_exit_reason_failed_fix_checks(self) -> None: + reason = mod._compute_lfg_exit_reason( + { + "lfg_merge_blocked": "pr_checks_failed", + "pr_ci_recommendation": {"action": "fix_checks"}, + }, + 3, + deferred=False, + ) + self.assertEqual(reason, "pr_checks_failed:fix_checks") + + def test_emit_lfg_strict_exit_stderr(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "pr_checks_pending:watch_queue", + "pr_ci_recommendation": {"command": "watch-cmd"}, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 3) + self.assertIn("code=3", err.getvalue()) + self.assertIn("watch_queue", err.getvalue()) + self.assertIn("watch-cmd", err.getvalue()) + + def test_emit_lfg_strict_exit_stderr_defer_briefing(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:unchanged_active_runs", + "lfg_agent_briefing": { + "primary_action": "gate_watch", + "expected_after_terminal": {"action": "closeout"}, + "active_runs": ["fc"], + "gh_watch_summary": "fc:26549293445", + "queue_context": {"max_queued_hours": 1.5, "note": "Runner backlog ~3h"}, + "watch_recommended": True, + "fc_run_id": 26549293445, + "verify_status": "queued", + "fc_status": "queued", + "blocked": "deferred", + "action": "defer", + "reason": "unchanged_active_runs", + "notes": ["Runner backlog ~3h"], + "merge_ready": False, + "monitor_commands": { + "watch_fc_run": "gh run watch 26549293445 --exit-status", + "gate_watch": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + }, + "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + output = err.getvalue() + self.assertIn("primary_action=gate_watch", output) + self.assertIn("expected_after=closeout", output) + self.assertIn("active_runs=fc", output) + self.assertIn("gh_watch=fc:26549293445", output) + self.assertIn("queued=1.5h", output) + self.assertIn("fc_run=26549293445", output) + self.assertIn("verify_status=queued", output) + self.assertIn("fc_status=queued", output) + self.assertIn("blocked=deferred", output) + self.assertIn("action=defer", output) + self.assertIn("briefing_reason=unchanged_active_runs", output) + self.assertIn("notes=1", output) + self.assertIn("merge_ready=false", output) + self.assertIn("queue_note=Runner backlog ~3h", output) + self.assertIn("watch=gh run watch 26549293445 --exit-status", output) + self.assertIn("briefing_command=", output) + self.assertIn("--lfg-gate-watch", output) + + def test_emit_lfg_strict_exit_stderr_prefers_top_level_status(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:fc_active_pending", + "primary_action": "gate_watch", + "max_queued_hours": 4.0, + "queue_backlog": True, + "lfg_agent_briefing": { + "primary_action": "legacy_action", + "queue_context": {"max_queued_hours": 1.0}, + }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + output = err.getvalue() + self.assertIn("primary_action=gate_watch", output) + self.assertNotIn("primary_action=legacy_action", output) + self.assertIn("queued=4.0h", output) + self.assertIn("queue_backlog=true", output) + + def test_lfg_briefing_mirror_stderr_parts_shared_helper(self) -> None: + status: dict[str, Any] = { + "primary_action": "gate_watch", + "briefing_action": "defer", + "max_queued_hours": 2.0, + "queue_backlog_warning": True, + } + parts = mod._lfg_briefing_mirror_stderr_parts(status) + joined = " ".join(parts) + self.assertIn("primary_action=gate_watch", joined) + self.assertIn("action=defer", joined) + self.assertIn("queued=2.0h", joined) + self.assertIn("queue_warn=true", joined) + + def test_lfg_briefing_mirror_stderr_parts_wait_drift(self) -> None: + status: dict[str, Any] = { + "briefing_action": "investigate_ci_drift", + "wait_recommended": True, + "ci_drift": { + "fields": [ + {"field": "forward_commits_run_id"}, + {"field": "verify_run_id"}, + ], + }, + "fc_run_id": 26549293445, + } + joined = " ".join(mod._lfg_briefing_mirror_stderr_parts(status)) + self.assertIn("wait=true", joined) + self.assertIn("drift_fields=forward_commits_run_id,verify_run_id", joined) + self.assertIn("fc_run=26549293445", joined) + + def test_emit_lfg_strict_exit_stderr_investigate_drift(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:fc_active_pending", + "briefing_action": "investigate_ci_drift", + "wait_recommended": True, + "ci_drift": { + "fields": [{"field": "forward_commits_run_id"}], + }, + "fc_run_id": 26547437912, + "lfg_agent_briefing": { + "action": "investigate_ci_drift", + "wait_recommended": True, + "drift": {"fields": [{"field": "forward_commits_run_id"}]}, + }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + output = err.getvalue() + self.assertIn("wait=true", output) + self.assertIn("drift_fields=forward_commits_run_id", output) + self.assertIn("action=investigate_ci_drift", output) + self.assertIn("fc_run=26547437912", output) + + def test_emit_lfg_strict_exit_stderr_watch_recommended(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:unchanged_active_runs", + "lfg_agent_briefing": {"watch_recommended": True}, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + self.assertIn("watch_recommended=true", err.getvalue()) + + def test_apply_lfg_agent_briefing_gh_watch_summary(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": {"defer_lfg_pr": True, "queue_backlog_note": "Runner backlog ~3h"}, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 2.5, "url": "https://example.com/runs/1"}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 0.3, "url": "https://example.com/runs/2"}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + mod._apply_lfg_agent_briefing(status) + briefing = status.get("lfg_agent_briefing") or {} + self.assertEqual(briefing.get("gh_watch_summary"), "verify:1,fc:2") + self.assertEqual(status.get("gh_watch_summary"), "verify:1,fc:2") + self.assertEqual(status.get("active_runs"), ["verify", "fc"]) + queue_context = status.get("queue_context") or {} + self.assertIn("max_queued_hours", queue_context) + self.assertTrue(status.get("queue_backlog_warning")) + self.assertFalse(status.get("queue_backlog")) + self.assertEqual(status.get("max_queued_hours"), queue_context.get("max_queued_hours")) + expected_after = status.get("expected_after_terminal") or {} + self.assertEqual(expected_after.get("action"), "closeout") + self.assertEqual(status.get("primary_action"), "gate_watch") + self.assertTrue(status.get("watch_recommended")) + post_terminal = status.get("post_terminal_commands") or {} + self.assertIn("closeout", post_terminal) + self.assertIn("--lfg-gate-watch", status.get("wait_command") or "") + self.assertIn("--lfg-gate-watch", status.get("briefing_command") or "") + monitor_commands = status.get("monitor_commands") or {} + self.assertIn("gate_watch", monitor_commands) + self.assertEqual(status.get("verify_run_id"), 1) + self.assertEqual(status.get("fc_run_id"), 2) + self.assertEqual(status.get("verify_run_url"), "https://example.com/runs/1") + self.assertEqual(status.get("fc_run_url"), "https://example.com/runs/2") + self.assertEqual(status.get("verify_status"), "queued") + self.assertEqual(status.get("fc_status"), "queued") + self.assertEqual(status.get("blocked"), "deferred") + self.assertEqual(status.get("briefing_action"), "defer") + self.assertEqual(status.get("briefing_reason"), "unchanged_active_runs") + self.assertEqual(status.get("briefing_notes"), ["Runner backlog ~3h"]) + self.assertFalse(status.get("briefing_merge_ready")) + self.assertEqual(status.get("queue_backlog_note"), "Runner backlog ~3h") + self.assertEqual( + status.get("gh_watch_command"), + "gh run watch 2 --exit-status", + ) + + def test_watch_pr_merge_status_conflicts(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "url": "https://example.com/pr/308", + "lfg_merge_blocked": "pr_merge_conflicts", + "pr_merge_ready": False, + }, + ): + mod._watch_pr_merge_status( + status, interval_sec=0.0, timeout_sec=60.0, stall_polls=99 + ) + self.assertEqual(status["lfg_pr_watch_result"], "pr_merge_conflicts") + + def test_check_detail_record_started_at(self) -> None: + detail = mod._check_detail_record( + { + "name": "build", + "startedAt": "2026-05-24T12:00:00Z", + "detailsUrl": "https://example.com/job/1", + "workflowName": "CI", + } + ) + self.assertEqual(detail["started_at"], "2026-05-24T12:00:00Z") + empty = mod._check_detail_record({"name": "queued", "startedAt": "0001-01-01T00:00:00Z"}) + self.assertEqual(empty["started_at"], "") + + def test_build_pr_ci_bottlenecks_sorted(self) -> None: + pr_status = { + "in_progress_check_details": [ + {"name": "new", "started_at": "2026-05-24T13:00:00Z", "workflow": "CI"}, + {"name": "old", "started_at": "2026-05-24T12:00:00Z", "workflow": "CI"}, + ], + "pending_check_details": [ + {"name": "queued", "started_at": "", "workflow": "CI"}, + ], + } + bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) + self.assertEqual(bottlenecks["in_progress"][0]["name"], "old") + self.assertEqual(bottlenecks["queued_longest_wait"][0]["name"], "queued") + self.assertFalse(bottlenecks["queue_backlog"]) + + def test_build_pr_ci_bottlenecks_queue_backlog(self) -> None: + pr_status = { + "checks_pending": 5, + "checks_in_progress": 0, + "in_progress_check_details": [], + "pending_check_details": [{"name": "label", "started_at": "", "workflow": "CI"}], + } + bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) + self.assertTrue(bottlenecks["queue_backlog"]) + self.assertIsNone(bottlenecks["oldest_queued_age_hours"]) + + def test_oldest_started_at_hours(self) -> None: + details = [ + {"started_at": "2026-05-27T20:00:00Z"}, + {"started_at": "2026-05-27T18:00:00Z"}, + ] + oldest_at, hours = mod._oldest_started_at_hours(details) + self.assertEqual(oldest_at, "2026-05-27T18:00:00Z") + self.assertIsNotNone(hours) + + def test_build_pr_ci_bottlenecks_oldest_age(self) -> None: + pr_status = { + "checks_pending": 2, + "checks_in_progress": 0, + "in_progress_check_details": [], + "pending_check_details": [ + {"name": "new", "started_at": "2026-05-27T22:00:00Z", "workflow": "CI"}, + {"name": "old", "started_at": "2026-05-27T20:00:00Z", "workflow": "CI"}, + ], + } + bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) + self.assertEqual(bottlenecks["oldest_queued_started_at"], "2026-05-27T20:00:00Z") + self.assertIsNotNone(bottlenecks["oldest_queued_age_hours"]) + + def test_fetch_pr_checks_crosscheck(self) -> None: + payload = [{"name": "build", "state": "QUEUED"}, {"name": "lint", "state": "SUCCESS"}] + with patch.object(mod.subprocess, "run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=["gh", "pr", "checks"], + returncode=0, + stdout=json.dumps(payload), + stderr="", + ) + cross = mod._fetch_pr_checks_crosscheck(308, 27) + self.assertTrue(cross["ok"]) + self.assertEqual(cross["gh_checks_total"], 2) + self.assertEqual(cross["rollup_vs_gh_delta"], 25) + self.assertEqual(cross["gh_state_counts"]["QUEUED"], 1) + + def test_build_pr_checks_crosscheck_note(self) -> None: + note = mod._build_pr_checks_crosscheck_note( + { + "ok": True, + "rollup_checks_total": 28, + "gh_checks_total": 26, + "rollup_vs_gh_delta": 2, + "gh_state_counts": {"QUEUED": 25, "SKIPPED": 1}, + }, + queue_backlog=True, + ) + self.assertIn("delta +2", note) + self.assertIn("gh reports 25 QUEUED", note) + self.assertEqual( + mod._build_pr_checks_crosscheck_note( + {"ok": True, "rollup_vs_gh_delta": 0}, + queue_backlog=False, + ), + "", + ) + + def test_emit_lfg_strict_exit_stderr_crosscheck(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "pr_checks_pending:watch_queue", + "pr_ci_recommendation": {"command": "watch-cmd"}, + "pr_checks_crosscheck_note": "delta +2", + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 3) + output = err.getvalue() + self.assertIn("crosscheck=delta +2", output) + + def test_build_lfg_agent_briefing_watch_queue(self) -> None: + status: dict[str, Any] = { + "lfg_track_complete": True, + "lfg_merge_blocked": "pr_checks_pending", + "lfg_exit_code": 3, + "lfg_exit_reason": "pr_checks_pending:watch_queue", + "pr_queue_backlog_note": "runner backlog", + "pr_checks_crosscheck_note": "delta +2", + "pr_ci_recommendation": { + "action": "watch_queue", + "reason": "runner queue backlog", + "command": "watch-cmd", + }, + "pr_merge_status": { + "ok": True, + "number": 308, + "url": "https://example.com/pr/308", + "pr_merge_ready": False, + "checks_pending": 27, + "checks_in_progress": 0, + "pr_ci_progress": {"completion_percent": 4}, + }, + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertEqual(briefing["action"], "watch_queue") + self.assertEqual(briefing["exit_code"], 3) + self.assertEqual(len(briefing["notes"]), 2) + self.assertEqual(briefing["completion_percent"], 4) + + def test_build_lfg_agent_briefing_no_pr(self) -> None: + status: dict[str, Any] = { + "lfg_track_complete": True, + "lfg_merge_blocked": "no_open_pr", + "pr_merge_status": {"ok": False}, + "pr_ci_recommendation": { + "action": "no_pr", + "reason": "no open PR on branch", + "command": "", + }, + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertEqual(briefing["action"], "no_pr") + self.assertEqual(briefing["blocked"], "no_open_pr") + + def test_build_lfg_agent_briefing_merge_ready(self) -> None: + status: dict[str, Any] = { + "lfg_track_complete": True, + "lfg_exit_code": 0, + "lfg_exit_reason": "merge_ready", + "pr_ci_recommendation": { + "action": "merge", + "reason": "PR CI complete", + "command": "gh pr merge 308 --squash --auto", + }, + "pr_merge_status": { + "ok": True, + "number": 308, + "url": "https://example.com/pr/308", + "pr_merge_ready": True, + "pr_ci_progress": {"completion_percent": 100}, + }, + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertEqual(briefing["action"], "merge") + self.assertTrue(briefing["merge_ready"]) + self.assertIn("gh pr merge", briefing["command"]) + + def test_build_lfg_agent_briefing_blocked_refresh(self) -> None: + status: dict[str, Any] = { + "lfg_refresh_blocked": "classify_fc_stale_gap", + "proceed_hint": ( + "python3 .github/scripts/local_verify_pypi_slice.py " + "--prefetch-git --lfg-gate" + ), + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertEqual(briefing["action"], "blocked_refresh") + self.assertIn("--prefetch-git", briefing["command"]) + self.assertEqual(briefing["reason"], "classify_fc_stale_gap") + + def test_should_emit_lfg_agent_briefing_stderr(self) -> None: + self.assertTrue( + mod._should_emit_lfg_agent_briefing_stderr( + {"action": "blocked_refresh"}, + 0, + ) + ) + self.assertFalse( + mod._should_emit_lfg_agent_briefing_stderr( + {"action": "merge"}, + 0, + ) + ) + self.assertTrue( + mod._should_emit_lfg_agent_briefing_stderr( + {"action": "investigate_ci_drift"}, + 0, + ) + ) + + def test_build_lfg_agent_briefing_investigate_drift(self) -> None: + status: dict[str, Any] = { + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-refresh --dry-run", + "checkpoint": { + "proceed_reason": "investigate_ci_drift", + "ci_drift_note": "FC run 26543899770 vs doc 26365648344", + }, + "doc_validation": { + "drift": [ + { + "field": "forward_commits_run_id", + "doc": 26365648344, + "live": 26543899770, + } + ], + }, + "verify_pypi": { + "run_id": 26372746392, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 26543899770, + "status": "completed", + "conclusion": "success", + }, + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertEqual(briefing["action"], "investigate_ci_drift") + self.assertIn("26543899770", briefing["notes"][0]) + self.assertFalse(briefing["wait_recommended"]) + self.assertIn("closeout", briefing["refresh_commands"]) + expected_after = briefing.get("expected_after_terminal") + self.assertIsInstance(expected_after, dict) + assert isinstance(expected_after, dict) + self.assertEqual(expected_after["action"], "closeout") + drift = briefing["drift"] + self.assertEqual(len(drift["fields"]), 1) + + def test_build_lfg_agent_briefing_investigate_drift_active_fc(self) -> None: + status: dict[str, Any] = { + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-refresh --dry-run", + "checkpoint": { + "proceed_reason": "investigate_ci_drift", + "ci_drift_note": "FC run 26547437912 vs doc 26547345351", + }, + "doc_validation": { + "drift": [ + { + "field": "forward_commits_run_id", + "doc": 26547345351, + "live": 26547437912, + } + ], + }, + "verify_pypi": { + "run_id": 26372746392, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 26547437912, + "status": "queued", + "conclusion": "", + "url": "https://example.com/runs/26547437912", + }, + } + briefing = mod._build_lfg_agent_briefing(status) + self.assertTrue(briefing["wait_recommended"]) + self.assertIn("--lfg-gate-watch", briefing["command"]) + self.assertEqual(briefing["fc_run_id"], 26547437912) + self.assertEqual(briefing["primary_action"], "gate_watch") + self.assertIn("gate_watch", briefing["refresh_commands"]) + self.assertNotIn("closeout", briefing["refresh_commands"]) + expected_after = briefing.get("expected_after_terminal") + self.assertIsInstance(expected_after, dict) + assert isinstance(expected_after, dict) + self.assertEqual(expected_after["action"], "refresh_dry_run") + self.assertIn("queue_context", briefing) + self.assertEqual(briefing["active_runs"], ["fc"]) + + def test_apply_lfg_agent_briefing_wait_drift_top_level(self) -> None: + status: dict[str, Any] = { + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-refresh --dry-run", + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "doc_validation": { + "drift": [ + { + "field": "forward_commits_run_id", + "doc": 1, + "live": 2, + } + ], + }, + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 2, + "status": "queued", + "conclusion": "", + }, + } + mod._apply_lfg_agent_briefing(status) + self.assertTrue(status.get("wait_recommended")) + ci_drift = status.get("ci_drift") or {} + self.assertIn("fields", ci_drift) + + def test_mirror_preflight_watch_summary_wait_drift(self) -> None: + summary: dict[str, Any] = {"polls": 1} + status: dict[str, Any] = { + "wait_recommended": True, + "ci_drift": {"fields": [{"field": "forward_commits_run_id"}]}, + } + mod._mirror_preflight_watch_summary_from_status(status, summary) + self.assertTrue(summary.get("wait_recommended")) + self.assertEqual( + (summary.get("ci_drift") or {}).get("fields"), + [{"field": "forward_commits_run_id"}], + ) + + def test_emit_drift_briefing_stderr_wait_expected_after(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "investigate_ci_drift", + "wait_recommended": True, + "primary_action": "gate_watch", + "active_runs": ["fc"], + "queue_context": {"max_queued_hours": 0.5}, + "expected_after_terminal": { + "action": "refresh_dry_run", + "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-refresh --dry-run", + }, + "drift": { + "fields": [ + {"field": "forward_commits_run_id"}, + {"field": "verify_run_id"}, + ], + }, + "fc_run_id": 26549293445, + "monitor_commands": { + "watch_fc_run": "gh run watch 26549293445 --exit-status", + }, + } + ) + output = err.getvalue() + self.assertIn("wait=true", output) + self.assertIn("primary_action=gate_watch", output) + self.assertIn("expected_after=refresh_dry_run", output) + self.assertIn("drift_fields=forward_commits_run_id,verify_run_id", output) + self.assertIn("queued=0.5h", output) + self.assertIn("active_runs=fc", output) + + def test_emit_defer_briefing_stderr_verify_run(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "unchanged_active_runs", + "verify_run_id": 26549547772, + "fc_run_id": 26549293445, + "active_runs": ["verify", "fc"], + "monitor_commands": { + "watch_verify_run": "gh run watch 26549547772 --exit-status", + "watch_fc_run": "gh run watch 26549293445 --exit-status", + }, + } + ) + output = err.getvalue() + self.assertIn("verify_run=26549547772", output) + self.assertIn("fc_run=26549293445", output) + self.assertIn("gh_watch=verify:26549547772,fc:26549293445", output) + + def test_emit_lfg_agent_briefing_stderr_prefers_top_level_status(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "verify_run_id": 999, + "fc_run_id": 1000, + "gh_watch_summary": "verify:999,fc:1000", + "lfg_agent_briefing": { + "action": "defer", + "reason": "unchanged_active_runs", + "verify_run_id": 1, + "fc_run_id": 2, + }, + } + ) + output = err.getvalue() + self.assertIn("verify_run=999", output) + self.assertIn("fc_run=1000", output) + self.assertIn("gh_watch=verify:999,fc:1000", output) + self.assertIn("reason=unchanged_active_runs", output) + + def test_format_gh_watch_summary_fc_only(self) -> None: + summary = mod._format_gh_watch_summary( + { + "fc_run_id": 26549293445, + "monitor_commands": { + "watch_fc_run": "gh run watch 26549293445 --exit-status", + }, + } + ) + self.assertEqual(summary, "fc:26549293445") + + def test_build_gh_watch_from_status(self) -> None: + status = { + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": ""}, + "forward_commits": {"run_id": 2, "status": "in_progress", "conclusion": ""}, + } + self.assertEqual(mod._build_gh_watch_from_status(status), "verify:1,fc:2") + + def test_mirror_preflight_watch_summary_from_status(self) -> None: + summary: dict[str, Any] = {"polls": 1} + status: dict[str, Any] = { + "active_runs": ["fc"], + "gh_watch_summary": "fc:99", + "primary_action": "gate_watch", + "verify_run_id": 99, + "fc_run_id": 100, + "briefing_action": "defer", + "briefing_reason": "fc_active_pending", + "briefing_notes": ["note"], + "briefing_merge_ready": False, + "blocked": "deferred", + "watch_recommended": True, + "max_queued_hours": 3.5, + "queue_backlog_warning": True, + "queue_context": {"max_queued_hours": 3.5, "queue_backlog_warning": True}, + "gh_watch_command": "gh run watch 100 --exit-status", + } + mod._mirror_preflight_watch_summary_from_status(status, summary) + self.assertEqual(summary.get("active_runs"), ["fc"]) + self.assertEqual(summary.get("gh_watch_summary"), "fc:99") + self.assertEqual(summary.get("primary_action"), "gate_watch") + self.assertEqual(summary.get("verify_run_id"), 99) + self.assertEqual(summary.get("fc_run_id"), 100) + self.assertEqual(summary.get("briefing_action"), "defer") + self.assertEqual(summary.get("briefing_reason"), "fc_active_pending") + self.assertEqual(summary.get("briefing_notes"), ["note"]) + self.assertFalse(summary.get("briefing_merge_ready")) + self.assertEqual(summary.get("blocked"), "deferred") + self.assertTrue(summary.get("watch_recommended")) + self.assertEqual(summary.get("max_queued_hours"), 3.5) + self.assertTrue(summary.get("queue_backlog_warning")) + self.assertEqual( + summary.get("gh_watch_command"), + "gh run watch 100 --exit-status", + ) + + def test_watch_summary_includes_active_runs(self) -> None: + deferred_status = { + "gh_ok": True, + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + "queue_backlog_note": "Runner backlog ~3h", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 2.5, "url": "https://example.com/runs/1"}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0, "url": "https://example.com/runs/2"}, + } + with patch.object(mod, "_ci_status", return_value=deferred_status): + with patch.object(mod, "_refine_lfg_checkpoint"): + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + with patch.object(mod.time, "sleep"): + with patch.object(mod.time, "monotonic", side_effect=[0.0, 0.0, 100.0]): + status = mod._watch_lfg_preflight_defer( + targets=["solution"], + prefetch_git=False, + interval_sec=0.0, + timeout_sec=5.0, + ) + summary = status.get("preflight_watch_summary") or {} + self.assertEqual(summary.get("active_runs"), ["verify", "fc"]) + self.assertEqual(summary.get("gh_watch_summary"), "verify:1,fc:2") + queue_context = summary.get("queue_context") or {} + self.assertEqual(queue_context.get("max_queued_hours"), 2.5) + expected_after = summary.get("expected_after_terminal") or {} + self.assertEqual(expected_after.get("action"), "closeout") + self.assertEqual(summary.get("primary_action"), "gate_watch") + self.assertTrue(summary.get("watch_recommended")) + post_terminal = summary.get("post_terminal_commands") or {} + self.assertIn("closeout", post_terminal) + self.assertIn("--lfg-gate-watch", summary.get("wait_command") or "") + monitor_commands = summary.get("monitor_commands") or {} + self.assertIn("gate_watch", monitor_commands) + self.assertEqual(summary.get("verify_run_id"), 1) + self.assertEqual(summary.get("fc_run_id"), 2) + self.assertEqual(summary.get("verify_run_url"), "https://example.com/runs/1") + self.assertEqual(summary.get("fc_run_url"), "https://example.com/runs/2") + self.assertEqual(summary.get("verify_status"), "queued") + self.assertEqual(summary.get("fc_status"), "queued") + self.assertEqual(summary.get("blocked"), "deferred") + self.assertEqual(summary.get("briefing_action"), "defer") + self.assertEqual(summary.get("briefing_reason"), "unchanged_active_runs") + self.assertEqual(summary.get("briefing_notes"), ["Runner backlog ~3h"]) + self.assertFalse(summary.get("briefing_merge_ready")) + self.assertEqual(summary.get("queue_backlog_note"), "Runner backlog ~3h") + self.assertTrue(summary.get("queue_backlog_warning")) + self.assertEqual(summary.get("max_queued_hours"), 2.5) + self.assertEqual( + summary.get("gh_watch_command"), + "gh run watch 2 --exit-status", + ) + self.assertIn("--lfg-gate-watch", summary.get("briefing_command") or "") + + def test_build_drift_expected_after_prefers_closeout(self) -> None: + expected = mod._build_drift_expected_after( + { + "refresh_dry_run": "dry-run", + "closeout": "closeout", + } + ) + self.assertIsNotNone(expected) + assert expected is not None + self.assertEqual(expected["action"], "closeout") + + def test_build_proceed_hint_investigate_drift_active_fc(self) -> None: + hint = mod._build_proceed_hint( + { + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "forward_commits": {"status": "queued", "conclusion": ""}, + "verify_pypi": {"status": "completed", "conclusion": "success"}, + }, + blocked=None, + ) + self.assertIn("--lfg-gate-watch", hint) + + def test_emit_investigate_drift_stderr_wait_and_fields(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "investigate_ci_drift", + "wait_recommended": True, + "drift": { + "fields": [{"field": "forward_commits_run_id"}], + }, + "fc_run_id": 26547437912, + "monitor_commands": { + "watch_fc_run": "gh run watch 26547437912 --exit-status", + }, + } + ) + output = err.getvalue() + self.assertIn("wait=true", output) + self.assertIn("drift_fields=forward_commits_run_id", output) + self.assertIn("fc_run=26547437912", output) + + def test_emit_lfg_agent_briefing_stderr(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "watch_queue", + "exit_code": 3, + "blocked": "pr_checks_pending", + "completion_percent": 4, + } + ) + output = err.getvalue() + self.assertIn("LFG briefing:", output) + self.assertIn("action=watch_queue", output) + self.assertIn("complete=4%", output) + + def test_apply_pr_merge_status_queue_backlog_hint(self) -> None: + recent_start = ( + datetime.now(timezone.utc) - timedelta(hours=1) + ).strftime("%Y-%m-%dT%H:%M:%SZ") + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "checks_pending": 26, + "checks_in_progress": 0, + "checks_total": 27, + "pending_checks": ["label"], + "pending_check_details": [ + { + "name": "label", + "started_at": recent_start, + "workflow": "CI", + "details_url": "", + }, + ], + "pr_merge_ready": False, + }, + ): + with patch.object( + mod, + "_fetch_pr_checks_crosscheck", + return_value={ + "ok": True, + "gh_checks_total": 25, + "rollup_checks_total": 27, + "rollup_vs_gh_delta": 2, + "gh_state_counts": {"QUEUED": 24}, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("runner backlog", status["merge_hint"]) + self.assertIn("oldest ~", status["merge_hint"]) + self.assertIn("pr_checks_crosscheck", status) + rec = status.get("pr_ci_recommendation") or {} + self.assertEqual(rec.get("action"), "watch_queue") + self.assertIn("pr_queue_backlog_note", status) + self.assertIn("pr_checks_crosscheck_note", status) + self.assertIn("delta +2", status["merge_hint"]) + self.assertIn("gh reports 24 QUEUED", status["pr_checks_crosscheck_note"]) + + def test_pr_ci_recommendation_merge_ready(self) -> None: + status: dict[str, Any] = { + "pr_merge_status": {"ok": True, "pr_merge_ready": True}, + "merge_actions": {"merge_squash_auto": "gh pr merge 308 --squash --auto"}, + } + rec = mod._build_pr_ci_recommendation(status) + self.assertEqual(rec["action"], "merge") + self.assertIn("gh pr merge", rec["command"]) + + def test_pr_ci_recommendation_defer_severe(self) -> None: + status: dict[str, Any] = { + "pr_merge_status": {"ok": True, "lfg_merge_blocked": "pr_checks_pending"}, + "lfg_merge_blocked": "pr_checks_pending", + "pr_ci_bottlenecks": { + "queue_backlog": True, + "queue_backlog_severe": True, + "oldest_queued_age_hours": 5.0, + }, + "merge_actions": {}, + } + rec = mod._build_pr_ci_recommendation(status) + self.assertEqual(rec["action"], "defer_external") + + def test_build_pr_queue_backlog_note(self) -> None: + note = mod._build_pr_queue_backlog_note( + { + "queue_backlog": True, + "oldest_queued_age_hours": 5.0, + "queue_backlog_severe": True, + } + ) + self.assertIn("severe", note) + self.assertIn("5.0h", note) + + def test_queue_backlog_severe(self) -> None: + pr_status = { + "checks_pending": 5, + "checks_in_progress": 0, + "in_progress_check_details": [], + "pending_check_details": [ + {"name": "old", "started_at": "2026-05-27T10:00:00Z", "workflow": "CI"}, + ], + } + with patch.object(mod, "_hours_since_iso", return_value=5.0): + bottlenecks = mod._build_pr_ci_bottlenecks(pr_status) + self.assertTrue(bottlenecks["queue_backlog_severe"]) + + def test_queue_stall_event_dedupe_by_pending(self) -> None: + events = [{"poll": 1, "checks_pending": 26, "hint": "backlog"}] + last_pending = events[-1].get("checks_pending") + self.assertFalse(last_pending != 26) + self.assertTrue(last_pending != 25) + + def test_build_pr_watch_summary_includes_crosscheck(self) -> None: + status: dict[str, Any] = { + "lfg_pr_watch_result": "ready", + "pr_watch_history": [ + {"completion_percent": 4, "checks_pending": 26}, + {"completion_percent": 100, "checks_pending": 0}, + ], + "pr_queue_stall_events": [], + "pr_watch_started_monotonic": mod.time.monotonic() - 10.0, + "pr_ci_bottlenecks": {"oldest_queued_age_hours": 0.5, "queue_backlog_severe": False}, + "pr_checks_crosscheck": {"rollup_vs_gh_delta": 2}, + } + summary = mod._build_pr_watch_summary(status) + self.assertEqual(summary.get("rollup_vs_gh_delta"), 2) + self.assertFalse(summary.get("queue_backlog_severe")) + + def test_evaluate_pr_watch_stall_queue(self) -> None: + recent = [ + { + "completion_percent": 4, + "checks_pending": 26, + "checks_in_progress": 0, + } + for _ in range(3) + ] + stall = mod._evaluate_pr_watch_stall( + recent, + stall_polls=3, + interval_sec=30.0, + bottlenecks={ + "in_progress": [], + "queued_longest_wait": [{"name": "label"}], + }, + next_name="label", + ) + self.assertIsNotNone(stall) + assert stall is not None + self.assertEqual(stall["lfg_pr_watch_result"], "queue_stalled") + self.assertEqual(stall["lfg_merge_blocked"], "pr_queue_stalled") + self.assertIn("queue backlog", stall["merge_hint"]) + + def test_evaluate_pr_watch_stall_job_hang(self) -> None: + recent = [ + { + "completion_percent": 42, + "checks_pending": 10, + "checks_in_progress": 2, + } + for _ in range(3) + ] + stall = mod._evaluate_pr_watch_stall( + recent, + stall_polls=3, + interval_sec=30.0, + bottlenecks={ + "in_progress": [{"name": "CodeQL", "workflow": "Analyze"}], + "queued_longest_wait": [], + }, + next_name="CodeQL", + ) + self.assertIsNotNone(stall) + assert stall is not None + self.assertEqual(stall["lfg_pr_watch_result"], "stalled") + self.assertIn("job hang", stall["merge_hint"]) + + def test_resolve_merge_watch_default_timeout(self) -> None: + self.assertEqual( + mod._resolve_watch_timeout_seconds(None, lfg_merge_watch=True), + 7200.0, + ) + self.assertEqual( + mod._resolve_watch_timeout_seconds(None, lfg_merge_watch=False), + 1800.0, + ) + self.assertEqual( + mod._resolve_watch_timeout_seconds(900.0, lfg_merge_watch=True), + 900.0, + ) + + def test_build_pr_watch_summary(self) -> None: + status: dict[str, Any] = { + "lfg_pr_watch_result": "ready", + "pr_watch_history": [ + { + "completion_percent": 4, + "checks_pending": 26, + "checks_queued": 26, + }, + { + "completion_percent": 100, + "checks_pending": 0, + "checks_queued": 0, + }, + ], + "pr_queue_stall_events": [{"poll": 1, "hint": "backlog"}], + "pr_watch_started_monotonic": mod.time.monotonic() - 60.0, + } + summary = mod._build_pr_watch_summary(status) + self.assertEqual(summary["completion_percent_delta"], 96) + self.assertEqual(summary["checks_pending_delta"], -26) + self.assertEqual(summary["queue_stall_events"], 1) + self.assertEqual(summary["lfg_pr_watch_result"], "ready") + + def test_watch_pr_merge_status_queue_stall_exits_when_flagged(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + queue_progress = { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pr_merge_ready": False, + "checks_pending": 26, + "checks_in_progress": 0, + "pr_ci_progress": {"completion_percent": 4}, + "in_progress_check_details": [], + "pending_check_details": [ + {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, + ], + } + + with patch.object(mod, "_fetch_pr_merge_status", return_value=queue_progress): + with patch.object(mod.time, "sleep"): + with patch("sys.stderr", new_callable=io.StringIO) as err: + mod._watch_pr_merge_status( + status, + interval_sec=30.0, + timeout_sec=3600.0, + stall_polls=3, + exit_on_queue_stall=True, + ) + self.assertEqual(status["lfg_pr_watch_result"], "queue_stalled") + self.assertTrue(status["pr_queue_stalled"]) + self.assertIn("queue_backlog=label", err.getvalue()) + + def test_watch_pr_merge_status_continues_through_queue_stall(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + queue_progress = { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pr_merge_ready": False, + "checks_pending": 26, + "checks_in_progress": 0, + "pr_ci_progress": {"completion_percent": 4}, + "in_progress_check_details": [], + "pending_check_details": [ + {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, + ], + } + calls = {"n": 0} + + def fetch_side() -> dict[str, Any]: + calls["n"] += 1 + if calls["n"] <= 3: + return queue_progress + return { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "pr_merge_ready": True, + "lfg_merge_blocked": None, + } + + with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): + with patch.object(mod.time, "sleep"): + with patch("sys.stderr", new_callable=io.StringIO) as err: + mod._watch_pr_merge_status( + status, + interval_sec=30.0, + timeout_sec=3600.0, + stall_polls=3, + ) + self.assertEqual(status["lfg_pr_watch_result"], "ready") + self.assertTrue(status["pr_queue_stalled"]) + self.assertEqual(len(status["pr_queue_stall_events"]), 1) + self.assertIn("continuing watch", err.getvalue()) + + def test_watch_pr_merge_status_queue_timeout(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + queue_progress = { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pr_merge_ready": False, + "checks_pending": 26, + "checks_in_progress": 0, + "pr_ci_progress": {"completion_percent": 4}, + "in_progress_check_details": [], + "pending_check_details": [ + {"name": "label", "started_at": "", "workflow": "CI", "details_url": ""}, + ], + } + with patch.object(mod, "_fetch_pr_merge_status", return_value=queue_progress): + with patch.object(mod.time, "sleep"): + with patch.object(mod.time, "monotonic", side_effect=[0.0, 0.0, 1.0, 1.0]): + mod._watch_pr_merge_status( + status, + interval_sec=30.0, + timeout_sec=0.0, + stall_polls=99, + ) + self.assertEqual(status["lfg_pr_watch_result"], "queue_timeout") + self.assertEqual(status["lfg_merge_blocked"], "pr_queue_stalled") + self.assertIn("queue backlog", status["merge_hint"]) + + def test_watch_pr_merge_status_stalled(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + stalled_progress = { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pr_merge_ready": False, + "checks_pending": 10, + "checks_in_progress": 2, + "pr_ci_progress": {"completion_percent": 42}, + "in_progress_check_details": [ + { + "name": "CodeQL", + "started_at": "2026-05-24T10:00:00Z", + "workflow": "Analyze", + "details_url": "https://example.com/1", + }, + ], + "pending_check_details": [], + } + + with patch.object(mod, "_fetch_pr_merge_status", return_value=stalled_progress): + with patch.object(mod.time, "sleep"): + with patch("sys.stderr", new_callable=io.StringIO) as err: + mod._watch_pr_merge_status( + status, + interval_sec=30.0, + timeout_sec=3600.0, + stall_polls=3, + ) + self.assertEqual(status["lfg_pr_watch_result"], "stalled") + self.assertTrue(status["pr_watch_stalled"]) + self.assertIn("job hang", status["merge_hint"]) + self.assertEqual(len(status["pr_watch_history"]), 3) + self.assertIn("bottleneck=CodeQL", err.getvalue()) + self.assertIn("PR watch stalled:", err.getvalue()) + + def test_recompare_checkpoint_status(self) -> None: + status: dict[str, Any] = { + "verify_pypi": {"run_id": 1, "status": "completed", "conclusion": "success", "head_sha": "a"}, + "forward_commits": {"run_id": 2, "status": "completed", "conclusion": "success", "head_sha": "b"}, + } + with patch.object(mod, "_compare_checkpoint", return_value={"proceed_reason": "update_monitoring_docs"}): + with patch.object(mod, "_validate_checkpoint_doc", return_value={"doc_valid": True}): + with patch.object(mod, "_refine_lfg_checkpoint") as mock_refine: + mod._recompare_checkpoint_status(status, targets=["solution"]) + self.assertIn("checkpoint", status) + mock_refine.assert_called_once() + + def test_build_proceed_hint_classify_fc_prefetch(self) -> None: + hint = mod._build_proceed_hint({"checkpoint": {}}, blocked="classify_fc_stale_gap") + self.assertIn("--prefetch-git", hint) + self.assertIn("--lfg-gate", hint) + + def test_apply_pr_merge_status_when_track_complete(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pending_checks": ["Analyze (python)"], + "pr_merge_ready": False, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("pr_merge_status", status) + self.assertIn("Analyze (python)", status["merge_hint"]) + + def test_apply_pr_merge_ready_includes_merge_cmd(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + with patch.object( + mod, + "_fetch_pr_merge_status", + return_value={ + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "pr_merge_ready": True, + "lfg_merge_blocked": None, + }, + ): + mod._apply_pr_merge_status(status) + self.assertIn("gh pr merge 308 --squash --auto", status["merge_hint"]) + + def test_watch_pr_merge_status_ready(self) -> None: + status: dict[str, Any] = {"lfg_track_complete": True} + calls = {"n": 0} + + def fetch_side() -> dict[str, Any]: + calls["n"] += 1 + if calls["n"] == 1: + return { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "lfg_merge_blocked": "pr_checks_pending", + "pending_checks": ["build"], + "in_progress_check_details": [ + {"name": "build", "details_url": "https://example.com/job/1", "workflow": "CI"}, + ], + "pr_merge_ready": False, + } + return { + "ok": True, + "number": 308, + "url": "https://github.com/example/pr/308", + "pr_merge_ready": True, + "lfg_merge_blocked": None, + } + + with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): + with patch.object(mod.time, "sleep"): + with patch("sys.stderr", new_callable=io.StringIO) as err: + mod._watch_pr_merge_status( + status, + interval_sec=0.0, + timeout_sec=60.0, + stall_polls=99, + ) + self.assertEqual(status["lfg_pr_watch_result"], "ready") + self.assertEqual(status["pr_watch_polls"], 2) + self.assertEqual(len(status["pr_watch_history"]), 2) + summary = status.get("pr_watch_summary") or {} + self.assertEqual(summary.get("lfg_pr_watch_result"), "ready") + self.assertEqual(summary.get("polls"), 2) + self.assertIn("PR watch poll 1:", err.getvalue()) + self.assertIn("next=build", err.getvalue()) + + def test_refine_lfg_checkpoint_monitoring_complete(self) -> None: + status: dict[str, Any] = { + "verify_pypi": { + "run_id": 1, + "status": "completed", + "conclusion": "success", + "head_sha": "abc1234567890", + "url": "https://example.com/1", + }, + "forward_commits": { + "run_id": 2, + "status": "completed", + "conclusion": "success", + "head_sha": "def1234567890", + "url": "https://example.com/2", + }, + "checkpoint": { + "defer_lfg_pr": False, + "proceed_reason": "update_monitoring_docs", + "doc_update_recommended": True, + }, + "doc_validation": {"doc_valid": True, "drift": [], "status_drift": []}, + } + with patch.object(mod, "_doc_patch_would_change", return_value=False): + mod._refine_lfg_checkpoint(status, targets=["solution", "plan020"]) + self.assertEqual(status["checkpoint"]["proceed_reason"], "monitoring_complete") + self.assertFalse(status["checkpoint"]["doc_update_recommended"]) + + def test_apply_lfg_track_complete(self) -> None: + status: dict[str, Any] = { + "checkpoint": {"proceed_reason": "monitoring_complete"}, + } + mod._apply_lfg_track_complete(status) + self.assertTrue(status["lfg_track_complete"]) + + def test_apply_lfg_proceed_skips_monitoring_complete(self) -> None: + status: dict[str, Any] = { + "checkpoint": { + "defer_lfg_pr": False, + "proceed_reason": "monitoring_complete", + } + } + mod._apply_lfg_proceed(status) + self.assertNotIn("lfg_proceed", status) + + def test_build_proceed_hint_monitoring_complete(self) -> None: + hint = mod._build_proceed_hint( + {"checkpoint": {"proceed_reason": "monitoring_complete"}}, + blocked=None, + ) + self.assertIn("track complete", hint) + self.assertNotIn("--lfg-closeout", hint) + + def test_git_prefetch_origin_master(self) -> None: + with patch.object(mod.subprocess, "run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=["git", "fetch"], + returncode=0, + stdout="", + stderr="", + ) + result = mod._git_prefetch_origin_master() + self.assertTrue(result["ok"]) + mock_run.assert_called_once() + + def test_apply_lfg_proceed_sets_fields(self) -> None: + status: dict[str, Any] = { + "checkpoint": { + "defer_lfg_pr": False, + "proceed_reason": "update_monitoring_docs", + } + } + mod._apply_lfg_proceed(status) + self.assertTrue(status["lfg_proceed"]) + self.assertEqual(status["lfg_proceed_reason"], "update_monitoring_docs") + + def test_apply_lfg_proceed_skipped_when_deferred(self) -> None: + status: dict[str, Any] = {"checkpoint": {"defer_lfg_pr": True, "proceed_reason": "x"}} + mod._apply_lfg_proceed(status) + self.assertNotIn("lfg_proceed", status) + + def test_maybe_auto_apply_skips_when_deferred(self) -> None: + status: dict[str, Any] = { + "checkpoint": { + "defer_lfg_pr": True, + "proceed_reason": "update_monitoring_docs", + } + } + result = mod._maybe_auto_apply_on_proceed(status, write=False, targets=["solution"]) + self.assertIsNone(result) + + def test_maybe_auto_apply_on_terminal_proceed(self) -> None: + status = { + "verify_pypi": { + "run_id": 10, + "status": "completed", + "conclusion": "success", + "head_sha": "abc1234567890", + "url": "https://example.com/10", + }, + "forward_commits": { + "run_id": 20, + "status": "completed", + "conclusion": "success", + "head_sha": "def1234567890", + "url": "https://example.com/20", + }, + "checkpoint_snippet": "**2026-05-24:** verify [10](u) **success** on `abc1234`; FC [20](u) **success** on `def1234`.", + "checkpoint": { + "defer_lfg_pr": False, + "proceed_reason": "update_monitoring_docs", + "doc_update_recommended": True, + }, + "doc_validation": {"doc_valid": False, "drift": [], "status_drift": [{"field": "verify_status"}]}, + } + doc = """--- +title: Verify PyPI Regression Closeout +last_verified: 2026-01-01 +--- + +## CI canonical runs + +| Workflow | Run | Notes | +|----------|-----|-------| +| Verify PyPI | [1](https://example.com/1) | old | +| Forward Commits | [2](https://example.com/2) | old | + +## Last CI check (plan 066) + +**old snippet** + +## Track status +""" + with patch.object(mod, "SOLUTION_CLOSEOUT") as mock_path: + mock_path.is_file.return_value = True + mock_path.read_text.return_value = doc + mock_path.relative_to.return_value = Path("docs/solutions/testing/verify-pypi-regression-closeout.md") + with patch.object(mod, "PLAN_020") as mock_plan: + mock_plan.is_file.return_value = False + result = mod._maybe_auto_apply_on_proceed(status, write=False, targets=["solution"]) + self.assertIsNotNone(result) + assert result is not None + self.assertTrue(result["allowed"]) + self.assertTrue(result["dry_run"]) + + def test_patch_solution_closeout_updates_last_verified(self) -> None: + doc = """--- +title: Verify PyPI Regression Closeout +last_verified: 2026-01-01 +--- + +## CI canonical runs + +| Workflow | Run | Notes | +|----------|-----|-------| +| Verify PyPI | [1](https://example.com/1) | old | +| Forward Commits | [2](https://example.com/2) | old | + +## Last CI check (plan 066) + +**old snippet** + +## Track status +""" + status = { + "verify_pypi": { + "run_id": 10, + "status": "queued", + "conclusion": "", + "head_sha": "abc1234567890", + "url": "https://example.com/10", + }, + "forward_commits": { + "run_id": 20, + "status": "queued", + "conclusion": "", + "head_sha": "def1234567890", + "url": "https://example.com/20", + }, + } + snippet = mod._format_checkpoint_snippet(status) + _patched, changes = mod._patch_solution_closeout(doc, status, snippet) + self.assertTrue(changes["last_verified"]) + + def test_compare_queue_backlog_note(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "queued", + "conclusion": "", + "head_sha": _MASTER_SHA, + "queued_hours": 5.5, + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": _MASTER_SHA, + "queued_hours": 1.0, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=True): + result = mod._compare_checkpoint(status) + self.assertTrue(result["defer_lfg_pr"]) + self.assertIn("queue_backlog_note", result) + self.assertIn("verify queued", result["queue_backlog_note"]) + + def test_monitor_preflight_includes_snippet_by_default(self) -> None: + result = subprocess.run( + [sys.executable, str(SCRIPT_PATH), "--monitor-preflight"], + capture_output=True, + text=True, + cwd=REPO_ROOT, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + payload = json.loads(result.stdout) + self.assertIn("checkpoint_snippet", payload) + + def test_format_checkpoint_snippet(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "queued", + "head_sha": _MASTER_SHA, + "url": "https://example.com/verify", + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "head_sha": _FC_SHA, + "url": "https://example.com/fc", + }, + } + snippet = mod._format_checkpoint_snippet(status) + self.assertIn("26372746392", snippet) + self.assertIn("26365648344", snippet) + self.assertIn("abc1234", snippet) + self.assertIn(date.today().isoformat(), snippet) + + def test_commits_since_are_docs_only_same_sha(self) -> None: + self.assertTrue(mod._commits_since_are_docs_only("abc", "abc")) + + def test_commits_since_are_docs_only_docs_paths(self) -> None: + with patch("subprocess.run") as mock_run: + mock_run.side_effect = [ + mock.MagicMock(returncode=0, stdout="c0mmit1\nc0mmit2\n"), + mock.MagicMock(returncode=0, stdout="docs/plans/foo.md\n"), + mock.MagicMock(returncode=0, stdout="docs/solutions/bar.md\n"), + ] + result = mod._commits_since_are_docs_only("base", "head") + self.assertTrue(result) + + def test_commits_since_are_docs_only_non_docs_path(self) -> None: + with patch("subprocess.run") as mock_run: + mock_run.side_effect = [ + mock.MagicMock(returncode=0, stdout="c0mmit1\n"), + mock.MagicMock(returncode=0, stdout="Libraries/PyKotor/src/foo.py\n"), + ] + result = mod._commits_since_are_docs_only("base", "head") + self.assertFalse(result) + + def test_compare_fc_sha_stale_benign_when_docs_only(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "queued", + "conclusion": "", + "head_sha": _MASTER_SHA, + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=True): + result = mod._compare_checkpoint(status) + self.assertTrue(result["defer_lfg_pr"]) + self.assertTrue(result["fc_sha_stale"]) + self.assertTrue(result["fc_sha_stale_benign"]) + self.assertIn("docs-only", result.get("fc_sha_stale_note", "")) + + def test_compare_no_defer_when_fc_non_docs_stale(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "queued", + "conclusion": "", + "head_sha": _MASTER_SHA, + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=False): + result = mod._compare_checkpoint(status) + self.assertFalse(result["defer_lfg_pr"]) + self.assertIn("non-docs", result.get("defer_reason", "")) + + def test_validate_checkpoint_doc_no_drift(self) -> None: + status = { + "verify_pypi": {"run_id": 26372746392, "status": "queued", "conclusion": ""}, + "forward_commits": {"run_id": 26365648344, "status": "queued", "conclusion": ""}, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + with patch.object( + mod, + "_parse_last_ci_check_status_words", + return_value={"verify_status_word": "queued", "fc_status_word": "queued"}, + ): + result = mod._validate_checkpoint_doc(status) + self.assertTrue(result["doc_valid"]) + self.assertEqual(result["drift"], []) + self.assertEqual(result["status_drift"], []) + + def test_validate_checkpoint_doc_detects_drift(self) -> None: + status = { + "verify_pypi": {"run_id": 999}, + "forward_commits": {"run_id": 26365648344}, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + result = mod._validate_checkpoint_doc(status) + self.assertFalse(result["doc_valid"]) + self.assertEqual(len(result["drift"]), 1) + + def test_git_origin_master_sha_falls_back_to_local_master(self) -> None: + with patch("subprocess.run") as mock_run: + mock_run.side_effect = [ + mock.MagicMock(returncode=1, stdout=""), + mock.MagicMock(returncode=0, stdout="localmaster\n"), + ] + result = mod._git_origin_master_sha() + self.assertEqual(result, "localmaster") + + def test_validate_checkpoint_doc_cli(self) -> None: + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_PATH), + "--ci-status-only", + "--validate-checkpoint-doc", + "--json", + ], + capture_output=True, + text=True, + cwd=REPO_ROOT, + check=False, + ) + self.assertIn(result.returncode, (0, 2), msg=result.stderr) + payload = json.loads(result.stdout) + self.assertIn("doc_valid", payload) + + def test_ci_status_human_output_does_not_crash(self) -> None: + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_PATH), + "--ci-status-only", + "--compare-checkpoint", + ], + capture_output=True, + text=True, + cwd=REPO_ROOT, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("=== CI STATUS ===", result.stdout) + + def test_emit_checkpoint_snippet(self) -> None: + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_PATH), + "--ci-status-only", + "--emit-checkpoint-snippet", + ], + capture_output=True, + text=True, + cwd=REPO_ROOT, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("verify [", result.stdout) + self.assertIn("FC [", result.stdout) + + def test_parse_run_ids_from_last_ci_check(self) -> None: + with patch.object(mod, "SOLUTION_CLOSEOUT", Path("/unused")): + with patch.object(mod, "_last_ci_check_section", return_value=SAMPLE_LAST_CHECK): + result = mod._parse_solution_checkpoint_run_ids() + self.assertEqual(result["verify_run_id"], 26365458400) + self.assertEqual(result["forward_commits_run_id"], 26365648344) + + def test_parse_missing_section_returns_error(self) -> None: + with patch.object(mod, "_last_ci_check_section", return_value=""): + with patch.object(mod, "_parse_canonical_table_run_ids", return_value={"error": "no table"}): + result = mod._parse_solution_checkpoint_run_ids() + self.assertIn("error", result) + + def test_compare_defer_when_queued_and_ids_match(self) -> None: + status = { + "verify_pypi": { + "run_id": 26365458400, + "status": "queued", + "conclusion": "", + "head_sha": _STALE_VERIFY_SHA, + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": _STALE_VERIFY_SHA, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26365458400, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_STALE_VERIFY_SHA): + result = mod._compare_checkpoint(status) + self.assertTrue(result["defer_lfg_pr"]) + self.assertTrue(result["checkpoint_unchanged"]) + + def test_compare_defer_when_in_progress_and_ids_match(self) -> None: + status = { + "verify_pypi": { + "run_id": 26365458400, + "status": "in_progress", + "conclusion": "", + "head_sha": "abc123", + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": "abc123", + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26365458400, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): + result = mod._compare_checkpoint(status) + self.assertTrue(result["defer_lfg_pr"]) + + def test_compare_no_defer_when_verify_sha_stale(self) -> None: + status = { + "verify_pypi": { + "run_id": 26365458400, + "status": "queued", + "conclusion": "", + "head_sha": _STALE_VERIFY_SHA, + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26365458400, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + result = mod._compare_checkpoint(status) + self.assertFalse(result["defer_lfg_pr"]) + self.assertTrue(result["verify_sha_stale"]) + self.assertIn("workflow_dispatch", result.get("recommended_action", "")) + + def test_compare_defer_when_fc_active_verify_completed_same_sha(self) -> None: + status = { + "verify_pypi": { + "run_id": 26365458400, + "status": "completed", + "conclusion": "success", + "head_sha": "abc123", + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": "abc123", + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26365458400, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): + result = mod._compare_checkpoint(status) + self.assertTrue(result["defer_lfg_pr"]) + self.assertIn("fc_active_closeout_note", result) + + def test_compare_no_defer_on_run_id_drift(self) -> None: + status = { + "verify_pypi": { + "run_id": 99999999999, + "status": "queued", + "conclusion": "", + "head_sha": "abc123", + }, + "forward_commits": { + "run_id": 26365648344, + "status": "queued", + "conclusion": "", + "head_sha": "abc123", + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26365458400, + "forward_commits_run_id": 26365648344, } + with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): + result = mod._compare_checkpoint(status) + self.assertFalse(result["defer_lfg_pr"]) + self.assertEqual(result.get("proceed_reason"), "investigate_ci_drift") + self.assertIn("ci_drift_note", result) - with patch.object(mod, "_fetch_pr_merge_status", side_effect=fetch_side): - with patch.object(mod.time, "sleep"): - with patch("sys.stderr", new_callable=io.StringIO) as err: - mod._watch_pr_merge_status( - status, - interval_sec=0.0, - timeout_sec=60.0, - stall_polls=99, - ) - self.assertEqual(status["lfg_pr_watch_result"], "ready") - self.assertEqual(status["pr_watch_polls"], 2) - self.assertEqual(len(status["pr_watch_history"]), 2) - summary = status.get("pr_watch_summary") or {} - self.assertEqual(summary.get("lfg_pr_watch_result"), "ready") - self.assertEqual(summary.get("polls"), 2) - self.assertIn("PR watch poll 1:", err.getvalue()) - self.assertIn("next=build", err.getvalue()) - - def test_refine_lfg_checkpoint_monitoring_complete(self) -> None: - status: dict[str, Any] = { + def test_compare_investigate_drift_before_fc_classify_gap(self) -> None: + status = { "verify_pypi": { - "run_id": 1, + "run_id": 26372746392, "status": "completed", "conclusion": "success", - "head_sha": "abc1234567890", - "url": "https://example.com/1", + "head_sha": _MASTER_SHA, }, "forward_commits": { - "run_id": 2, + "run_id": 26543899770, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, + }, + } + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26365648344, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=None): + result = mod._compare_checkpoint(status) + self.assertEqual(result.get("proceed_reason"), "investigate_ci_drift") + self.assertIn("26543899770", result.get("ci_drift_note", "")) + + def test_compare_defer_classify_gap_when_fc_active(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, "status": "completed", "conclusion": "success", - "head_sha": "def1234567890", - "url": "https://example.com/2", + "head_sha": _MASTER_SHA, }, - "checkpoint": { - "defer_lfg_pr": False, - "proceed_reason": "update_monitoring_docs", - "doc_update_recommended": True, + "forward_commits": { + "run_id": 26543899770, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, }, - "doc_validation": {"doc_valid": True, "drift": [], "status_drift": []}, } - with patch.object(mod, "_doc_patch_would_change", return_value=False): - mod._refine_lfg_checkpoint(status, targets=["solution", "plan020"]) - self.assertEqual(status["checkpoint"]["proceed_reason"], "monitoring_complete") - self.assertFalse(status["checkpoint"]["doc_update_recommended"]) + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26543899770, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=None): + result = mod._compare_checkpoint(status) + self.assertTrue(result.get("defer_lfg_pr")) + self.assertNotIn("proceed_reason", result) + self.assertIn("fc_stale_gap_pending_note", result) + self.assertIn("queued", result.get("fc_stale_gap_pending_note", "")) - def test_apply_lfg_track_complete(self) -> None: - status: dict[str, Any] = { - "checkpoint": {"proceed_reason": "monitoring_complete"}, + def test_compare_fc_active_pending_queue_backlog_note(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "completed", + "conclusion": "success", + "head_sha": _MASTER_SHA, + }, + "forward_commits": { + "run_id": 26543899770, + "status": "queued", + "conclusion": "", + "head_sha": _FC_SHA, + "queued_hours": 4.5, + }, } - mod._apply_lfg_track_complete(status) - self.assertTrue(status["lfg_track_complete"]) - - def test_apply_lfg_proceed_skips_monitoring_complete(self) -> None: - status: dict[str, Any] = { - "checkpoint": { - "defer_lfg_pr": False, - "proceed_reason": "monitoring_complete", + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26543899770, } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=None): + result = mod._compare_checkpoint(status) + self.assertIn("queue_backlog_note", result) + self.assertIn("4.5h", result["queue_backlog_note"]) + + def test_compare_classify_gap_when_fc_terminal_benign_unknown(self) -> None: + status = { + "verify_pypi": { + "run_id": 26372746392, + "status": "completed", + "conclusion": "success", + "head_sha": _MASTER_SHA, + }, + "forward_commits": { + "run_id": 26543899770, + "status": "completed", + "conclusion": "success", + "head_sha": _FC_SHA, + }, } - mod._apply_lfg_proceed(status) - self.assertNotIn("lfg_proceed", status) + with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: + mock_parse.return_value = { + "verify_run_id": 26372746392, + "forward_commits_run_id": 26543899770, + } + with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): + with patch.object(mod, "_commits_since_are_docs_only", return_value=None): + result = mod._compare_checkpoint(status) + self.assertFalse(result.get("defer_lfg_pr")) + self.assertEqual(result.get("proceed_reason"), "classify_fc_stale_gap") + self.assertIn("fc_stale_gap_note", result) - def test_build_proceed_hint_monitoring_complete(self) -> None: - hint = mod._build_proceed_hint( - {"checkpoint": {"proceed_reason": "monitoring_complete"}}, - blocked=None, + def test_build_lfg_agent_briefing_defer_fc_active_pending(self) -> None: + briefing = mod._build_lfg_agent_briefing( + { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "proceed_hint": ( + "python3 .github/scripts/local_verify_pypi_slice.py " + "--lfg-gate-watch --json # poll until active runs reach terminal" + ), + "checkpoint": { + "fc_stale_gap_pending_note": "FC queued on def1234 vs master abc1234", + "fc_sha_stale": True, + }, + "forward_commits": { + "run_id": 26546235822, + "status": "queued", + "conclusion": "", + "url": "https://example.com/runs/26546235822", + }, + } ) - self.assertIn("track complete", hint) - self.assertNotIn("--lfg-closeout", hint) + self.assertEqual(briefing["action"], "defer") + self.assertEqual(briefing["reason"], "fc_active_pending") + self.assertIn("FC queued", briefing["notes"][0]) + self.assertEqual(briefing["fc_run_id"], 26546235822) + self.assertEqual(briefing["fc_run_url"], "https://example.com/runs/26546235822") + self.assertEqual(briefing["fc_status"], "queued") + monitor = briefing["monitor_commands"] + self.assertIn("preflight_retry", monitor) + self.assertEqual( + monitor["watch_fc_run"], + "gh run watch 26546235822 --exit-status", + ) + self.assertIn("preflight_watch", monitor) + self.assertIn("--lfg-preflight-watch", monitor["preflight_watch"]) + self.assertIn("gate_watch", monitor) + self.assertIn("--lfg-gate-watch", monitor["gate_watch"]) + self.assertIn("prefetch_gate", briefing["post_terminal_commands"]) + self.assertNotIn("preflight-watch", monitor["preflight_retry"]) + self.assertTrue(briefing["watch_recommended"]) + self.assertEqual(briefing["primary_action"], "gate_watch") + self.assertIn("--lfg-gate-watch", briefing["command"]) + sha_gap = briefing["sha_gap"] + self.assertEqual(sha_gap["fc_head_sha"], None) + self.assertTrue(sha_gap["fc_sha_stale"]) + + def test_build_defer_sha_gap_detail_fc_active(self) -> None: + detail = mod._build_defer_sha_gap_detail( + { + "checkpoint": { + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", + "fc_stale_gap_pending_note": "FC queued on 7d85438 vs master 8916e2f", + }, + "forward_commits": { + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "queued_hours": 0.12, + }, + } + ) + self.assertIsNotNone(detail) + assert detail is not None + self.assertEqual(detail["short"], "7d85438:8916e2f") + self.assertEqual(detail["queued_hours"], 0.12) - def test_git_prefetch_origin_master(self) -> None: - with patch.object(mod.subprocess, "run") as mock_run: - mock_run.return_value = subprocess.CompletedProcess( - args=["git", "fetch"], - returncode=0, - stdout="", - stderr="", - ) - result = mod._git_prefetch_origin_master() - self.assertTrue(result["ok"]) - mock_run.assert_called_once() + def test_build_lfg_agent_briefing_defer_fc_active_sha_gap(self) -> None: + briefing = mod._build_lfg_agent_briefing( + { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight-watch --json", + "checkpoint": { + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", + "fc_stale_gap_pending_note": "FC queued on 7d85438 vs master 8916e2f", + }, + "forward_commits": { + "run_id": 26547475742, + "status": "queued", + "conclusion": "", + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "url": "https://example.com/runs/26547475742", + "queued_hours": 0.1, + }, + } + ) + self.assertIn("sha_gap", briefing) + self.assertEqual(briefing["sha_gap"]["short"], "7d85438:8916e2f") - def test_apply_lfg_proceed_sets_fields(self) -> None: + def test_apply_lfg_agent_briefing_sha_gap_top_level(self) -> None: status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight-watch --json", "checkpoint": { - "defer_lfg_pr": False, - "proceed_reason": "update_monitoring_docs", - } + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", + "fc_stale_gap_pending_note": "FC queued on 7d85438 vs master 8916e2f", + }, + "forward_commits": { + "run_id": 26547475742, + "status": "queued", + "conclusion": "", + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "url": "https://example.com/runs/26547475742", + "queued_hours": 0.1, + }, } - mod._apply_lfg_proceed(status) - self.assertTrue(status["lfg_proceed"]) - self.assertEqual(status["lfg_proceed_reason"], "update_monitoring_docs") - - def test_apply_lfg_proceed_skipped_when_deferred(self) -> None: - status: dict[str, Any] = {"checkpoint": {"defer_lfg_pr": True, "proceed_reason": "x"}} - mod._apply_lfg_proceed(status) - self.assertNotIn("lfg_proceed", status) + mod._apply_lfg_agent_briefing(status) + self.assertEqual(status.get("sha_gap_short"), "7d85438:8916e2f") + sha_gap = status.get("sha_gap") or {} + self.assertEqual(sha_gap.get("short"), "7d85438:8916e2f") - def test_maybe_auto_apply_skips_when_deferred(self) -> None: + def test_emit_lfg_strict_exit_stderr_sha_gap(self) -> None: status: dict[str, Any] = { - "checkpoint": { - "defer_lfg_pr": True, - "proceed_reason": "update_monitoring_docs", - } + "lfg_exit_reason": "deferred:fc_active_pending", + "lfg_agent_briefing": { + "sha_gap": {"short": "7d85438:8916e2f"}, + }, } - result = mod._maybe_auto_apply_on_proceed(status, write=False, targets=["solution"]) - self.assertIsNone(result) + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + self.assertIn("sha_gap=7d85438:8916e2f", err.getvalue()) - def test_maybe_auto_apply_on_terminal_proceed(self) -> None: - status = { - "verify_pypi": { - "run_id": 10, - "status": "completed", - "conclusion": "success", - "head_sha": "abc1234567890", - "url": "https://example.com/10", - }, + def test_extract_gh_watch_command_prefers_fc(self) -> None: + command = mod._extract_gh_watch_command( + { + "monitor_commands": { + "watch_verify_run": "gh run watch 1 --exit-status", + "watch_fc_run": "gh run watch 2 --exit-status", + } + } + ) + self.assertEqual(command, "gh run watch 2 --exit-status") + + def test_extract_gh_watch_command_verify_only(self) -> None: + command = mod._extract_gh_watch_command( + { + "monitor_commands": { + "watch_verify_run": "gh run watch 1 --exit-status", + } + } + ) + self.assertEqual(command, "gh run watch 1 --exit-status") + + def test_apply_lfg_agent_briefing_gh_watch_command_top_level(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight-watch --json", "forward_commits": { - "run_id": 20, - "status": "completed", - "conclusion": "success", - "head_sha": "def1234567890", - "url": "https://example.com/20", + "run_id": 26546235822, + "status": "queued", + "conclusion": "", + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "url": "https://example.com/runs/26546235822", + "queued_hours": 0.1, }, - "checkpoint_snippet": "**2026-05-24:** verify [10](u) **success** on `abc1234`; FC [20](u) **success** on `def1234`.", "checkpoint": { - "defer_lfg_pr": False, - "proceed_reason": "update_monitoring_docs", - "doc_update_recommended": True, + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", }, - "doc_validation": {"doc_valid": False, "drift": [], "status_drift": [{"field": "verify_status"}]}, } - doc = """--- -title: Verify PyPI Regression Closeout -last_verified: 2026-01-01 ---- + mod._apply_lfg_agent_briefing(status) + self.assertEqual( + status.get("gh_watch_command"), + "gh run watch 26546235822 --exit-status", + ) -## CI canonical runs + def test_emit_lfg_strict_exit_stderr_gh_watch_command(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:fc_active_pending", + "lfg_agent_briefing": { + "monitor_commands": { + "watch_fc_run": "gh run watch 26546235822 --exit-status", + } + }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + self.assertIn("watch=gh run watch 26546235822 --exit-status", err.getvalue()) -| Workflow | Run | Notes | -|----------|-----|-------| -| Verify PyPI | [1](https://example.com/1) | old | -| Forward Commits | [2](https://example.com/2) | old | + def test_format_preflight_watch_summary_line_gh_watch_command(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "gh_watch_command": "gh run watch 26546235822 --exit-status", + } + ) + self.assertIn("watch=gh run watch 26546235822 --exit-status", line) -## Last CI check (plan 066) + def test_format_briefing_command_stderr_truncates(self) -> None: + long_command = "python3 " + ("x" * 100) + formatted = mod._format_briefing_command_stderr(long_command) + self.assertTrue(formatted.endswith("...")) + self.assertLessEqual(len(formatted), 96) -**old snippet** + def test_emit_lfg_strict_exit_stderr_briefing_command(self) -> None: + status: dict[str, Any] = { + "lfg_exit_reason": "deferred:unchanged_active_runs", + "lfg_agent_briefing": { + "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + }, + } + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_strict_exit_stderr(status, 2) + self.assertIn("briefing_command=", err.getvalue()) + self.assertIn("--lfg-gate-watch", err.getvalue()) -## Track status -""" - with patch.object(mod, "SOLUTION_CLOSEOUT") as mock_path: - mock_path.is_file.return_value = True - mock_path.read_text.return_value = doc - mock_path.relative_to.return_value = Path("docs/solutions/testing/verify-pypi-regression-closeout.md") - with patch.object(mod, "PLAN_020") as mock_plan: - mock_plan.is_file.return_value = False - result = mod._maybe_auto_apply_on_proceed(status, write=False, targets=["solution"]) - self.assertIsNotNone(result) - assert result is not None - self.assertTrue(result["allowed"]) - self.assertTrue(result["dry_run"]) + def test_format_preflight_watch_summary_line_briefing_command(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "briefing_command": ( + "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json" + ), + } + ) + self.assertIn("briefing_command=", line) + self.assertIn("--lfg-gate-watch", line) - def test_patch_solution_closeout_updates_last_verified(self) -> None: - doc = """--- -title: Verify PyPI Regression Closeout -last_verified: 2026-01-01 ---- + def test_build_defer_queue_context_severe(self) -> None: + context = mod._build_defer_queue_context( + { + "forward_commits": {"queued_hours": 4.2}, + "checkpoint": {"queue_backlog_note": "FC queued 4.2h (external runner backlog)"}, + } + ) + self.assertTrue(context["queue_backlog"]) + self.assertFalse(context["queue_backlog_warning"]) + self.assertEqual(context["max_queued_hours"], 4.2) -## CI canonical runs + def test_build_defer_queue_context_warning(self) -> None: + context = mod._build_defer_queue_context( + {"forward_commits": {"queued_hours": 2.5}} + ) + self.assertFalse(context["queue_backlog"]) + self.assertTrue(context["queue_backlog_warning"]) + self.assertEqual(context["max_queued_hours"], 2.5) -| Workflow | Run | Notes | -|----------|-----|-------| -| Verify PyPI | [1](https://example.com/1) | old | -| Forward Commits | [2](https://example.com/2) | old | + def test_build_defer_expected_after_terminal_prefetch_gate(self) -> None: + expected = mod._build_defer_expected_after_terminal( + { + "preflight": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight --json", + "gate": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate", + "prefetch_gate": "python3 .github/scripts/local_verify_pypi_slice.py --prefetch-git --lfg-gate", + } + ) + self.assertIsNotNone(expected) + assert expected is not None + self.assertEqual(expected["action"], "prefetch_gate") + self.assertIn("--prefetch-git", expected["command"]) -## Last CI check (plan 066) + def test_build_defer_expected_after_terminal_prefers_closeout(self) -> None: + expected = mod._build_defer_expected_after_terminal( + { + "gate": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate", + "closeout": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-closeout", + } + ) + self.assertIsNotNone(expected) + assert expected is not None + self.assertEqual(expected["action"], "closeout") -**old snippet** + def test_build_active_runs_list(self) -> None: + active = mod._build_active_runs_list( + { + "verify_pypi": {"status": "completed", "conclusion": "success"}, + "forward_commits": {"status": "queued", "conclusion": ""}, + } + ) + self.assertEqual(active, ["fc"]) -## Track status -""" - status = { - "verify_pypi": { - "run_id": 10, - "status": "queued", - "conclusion": "", - "head_sha": "abc1234567890", - "url": "https://example.com/10", + def test_defer_briefing_unchanged_active_runs(self) -> None: + briefing = mod._build_lfg_agent_briefing( + { + "gh_ok": True, + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + "checkpoint": {"defer_lfg_pr": True}, + "verify_pypi": { + "run_id": 26372746392, + "status": "completed", + "conclusion": "success", + }, + "forward_commits": { + "run_id": 26549293445, + "status": "queued", + "conclusion": "", + "queued_hours": 0.3, + }, + } + ) + self.assertEqual(briefing["active_runs"], ["fc"]) + expected_after = briefing.get("expected_after_terminal") + self.assertIsInstance(expected_after, dict) + assert isinstance(expected_after, dict) + self.assertEqual(expected_after["action"], "closeout") + self.assertIn("closeout", briefing["post_terminal_commands"]) + + def test_emit_defer_briefing_stderr_active_runs(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "unchanged_active_runs", + "active_runs": ["fc"], + "expected_after_terminal": {"action": "closeout"}, + } + ) + self.assertIn("active_runs=fc", err.getvalue()) + self.assertIn("expected_after=closeout", err.getvalue()) + + def test_format_gate_watch_poll_line_active_runs(self) -> None: + line = mod._format_preflight_watch_poll_line( + 1, + { + "lfg_defer_reason": "unchanged_active_runs", + "verify_pypi": {"run_id": 1, "status": "completed", "conclusion": "success"}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": ""}, }, + watch_label="gate", + ) + self.assertIn("active_runs=fc", line) + + def test_defer_briefing_expected_after_terminal(self) -> None: + briefing = mod._build_lfg_agent_briefing( + { + "gh_ok": True, + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "proceed_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + "checkpoint": { + "fc_sha_stale": True, + "fc_stale_gap_pending_note": "FC queued on 573c9d4 vs master 8916e2f", + }, + "forward_commits": { + "run_id": 26548176325, + "status": "queued", + "conclusion": "", + "head_sha": "573c9d4bb474ed3ffdb871d3e081431a51f31702", + "queued_hours": 0.5, + }, + } + ) + expected_after = briefing.get("expected_after_terminal") + self.assertIsInstance(expected_after, dict) + assert isinstance(expected_after, dict) + self.assertEqual(expected_after["action"], "prefetch_gate") + + def test_emit_defer_briefing_stderr_expected_after_and_queue_warn(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "fc_active_pending", + "queue_context": { + "max_queued_hours": 2.5, + "queue_backlog_warning": True, + }, + "expected_after_terminal": { + "action": "prefetch_gate", + "command": "python3 .github/scripts/local_verify_pypi_slice.py --prefetch-git --lfg-gate", + }, + } + ) + output = err.getvalue() + self.assertIn("queue_warn=true", output) + self.assertIn("expected_after=prefetch_gate", output) + self.assertNotIn("queue_backlog=true", output) + + def test_emit_defer_briefing_stderr_queue_backlog(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "fc_active_pending", + "watch_recommended": True, + "primary_action": "gate_watch", + "queue_context": { + "queue_backlog_severe": True, + "max_queued_hours": 4.2, + }, + } + ) + output = err.getvalue() + self.assertIn("primary_action=gate_watch", output) + self.assertIn("queue_backlog=true", output) + self.assertIn("queued=4.2h", output) + + def test_emit_defer_briefing_stderr_queued_hours_not_severe(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "fc_active_pending", + "queue_context": {"max_queued_hours": 0.33}, + } + ) + output = err.getvalue() + self.assertIn("queued=0.3h", output) + self.assertNotIn("queue_backlog=true", output) + + def test_build_defer_monitor_commands_verify_active(self) -> None: + commands = mod._build_defer_monitor_commands( + { + "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight", + "verify_run_id": 26372746392, + } + ) + self.assertEqual( + commands["watch_verify_run"], + "gh run watch 26372746392 --exit-status", + ) + + def test_emit_defer_briefing_stderr_includes_reason_and_fc_run(self) -> None: + with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: + mod._emit_lfg_agent_briefing_stderr( + { + "action": "defer", + "reason": "fc_active_pending", + "blocked": "deferred", + "watch_recommended": True, + "fc_run_id": 26546235822, + "sha_gap": {"short": "7d85438:8916e2f"}, + "monitor_commands": { + "watch_fc_run": "gh run watch 26546235822 --exit-status", + }, + } + ) + output = err.getvalue() + self.assertIn("reason=fc_active_pending", output) + self.assertIn("watch_recommended=true", output) + self.assertIn("sha_gap=7d85438:8916e2f", output) + self.assertIn("fc_run=26546235822", output) + self.assertIn("watch=gh run watch 26546235822 --exit-status", output) + + def test_watch_lfg_preflight_defer_proceed(self) -> None: + deferred_status = { + "gh_ok": True, + "checkpoint": {"defer_lfg_pr": True, "defer_reason": "FC run still active"}, "forward_commits": { - "run_id": 20, + "run_id": 1, "status": "queued", "conclusion": "", - "head_sha": "def1234567890", - "url": "https://example.com/20", }, } - snippet = mod._format_checkpoint_snippet(status) - _patched, changes = mod._patch_solution_closeout(doc, status, snippet) - self.assertTrue(changes["last_verified"]) - - def test_compare_queue_backlog_note(self) -> None: - status = { - "verify_pypi": { - "run_id": 26372746392, - "status": "queued", - "conclusion": "", - "head_sha": _MASTER_SHA, - "queued_hours": 5.5, + proceed_status = { + "gh_ok": True, + "checkpoint": { + "defer_lfg_pr": False, + "proceed_reason": "update_monitoring_docs", }, "forward_commits": { - "run_id": 26365648344, - "status": "queued", - "conclusion": "", - "head_sha": _MASTER_SHA, - "queued_hours": 1.0, + "run_id": 1, + "status": "completed", + "conclusion": "success", }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=True): - result = mod._compare_checkpoint(status) - self.assertTrue(result["defer_lfg_pr"]) - self.assertIn("queue_backlog_note", result) - self.assertIn("verify queued", result["queue_backlog_note"]) + with patch.object(mod, "_ci_status", side_effect=[deferred_status, proceed_status]): + with patch.object(mod, "_refine_lfg_checkpoint"): + with patch.object(mod.time, "sleep"): + status = mod._watch_lfg_preflight_defer( + targets=["solution", "plan020"], + prefetch_git=False, + interval_sec=0.0, + timeout_sec=60.0, + ) + self.assertEqual(status["lfg_preflight_watch_result"], "proceed") + self.assertFalse(status.get("lfg_deferred")) + summary = status.get("preflight_watch_summary") or {} + self.assertEqual(summary.get("polls"), 2) + self.assertIn("next_hint", summary) + + def test_resolve_lfg_mode_gate_watch(self) -> None: + self.assertEqual( + mod._resolve_lfg_mode( + lfg_merge_watch=False, + lfg_merge_gate=False, + lfg_closeout=False, + lfg_gate=True, + lfg_gate_watch=True, + lfg_preflight=True, + lfg_preflight_watch=True, + lfg_refresh=True, + lfg_pr_watch=False, + dry_run=True, + ), + "gate_watch", + ) - def test_monitor_preflight_includes_snippet_by_default(self) -> None: - result = subprocess.run( - [sys.executable, str(SCRIPT_PATH), "--monitor-preflight"], - capture_output=True, - text=True, - cwd=REPO_ROOT, - check=False, + def test_build_defer_post_terminal_commands(self) -> None: + commands = mod._build_defer_post_terminal_commands( + {"checkpoint": {"fc_sha_stale": True}} ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - payload = json.loads(result.stdout) - self.assertIn("checkpoint_snippet", payload) + self.assertIn("prefetch_gate", commands) + self.assertIn("--prefetch-git", commands["prefetch_gate"]) - def test_format_checkpoint_snippet(self) -> None: - status = { - "verify_pypi": { - "run_id": 26372746392, - "status": "queued", - "head_sha": _MASTER_SHA, - "url": "https://example.com/verify", - }, - "forward_commits": { - "run_id": 26365648344, - "status": "queued", - "head_sha": _FC_SHA, - "url": "https://example.com/fc", - }, + def test_watch_lfg_preflight_defer_timeout(self) -> None: + deferred_status = { + "gh_ok": True, + "checkpoint": {"defer_lfg_pr": True}, + "forward_commits": {"run_id": 1, "status": "queued", "conclusion": ""}, } - snippet = mod._format_checkpoint_snippet(status) - self.assertIn("26372746392", snippet) - self.assertIn("26365648344", snippet) - self.assertIn("abc1234", snippet) - self.assertIn(date.today().isoformat(), snippet) + with patch.object(mod, "_ci_status", return_value=deferred_status): + with patch.object(mod, "_refine_lfg_checkpoint"): + with patch.object(mod.time, "sleep"): + with patch.object(mod.time, "monotonic", side_effect=[0.0, 0.0, 100.0]): + status = mod._watch_lfg_preflight_defer( + targets=["solution"], + prefetch_git=False, + interval_sec=0.0, + timeout_sec=5.0, + ) + self.assertEqual(status["lfg_preflight_watch_result"], "timeout") + self.assertTrue(status.get("lfg_deferred")) - def test_commits_since_are_docs_only_same_sha(self) -> None: - self.assertTrue(mod._commits_since_are_docs_only("abc", "abc")) + def test_resolve_lfg_mode_preflight_watch(self) -> None: + self.assertEqual( + mod._resolve_lfg_mode( + lfg_merge_watch=False, + lfg_merge_gate=False, + lfg_closeout=False, + lfg_gate=False, + lfg_gate_watch=False, + lfg_preflight=True, + lfg_preflight_watch=True, + lfg_refresh=True, + lfg_pr_watch=False, + dry_run=True, + ), + "preflight_watch", + ) - def test_commits_since_are_docs_only_docs_paths(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.side_effect = [ - mock.MagicMock(returncode=0, stdout="c0mmit1\nc0mmit2\n"), - mock.MagicMock(returncode=0, stdout="docs/plans/foo.md\n"), - mock.MagicMock(returncode=0, stdout="docs/solutions/bar.md\n"), - ] - result = mod._commits_since_are_docs_only("base", "head") - self.assertTrue(result) + def test_resolve_watch_timeout_preflight_watch(self) -> None: + self.assertEqual( + mod._resolve_watch_timeout_seconds( + None, + lfg_merge_watch=False, + lfg_preflight_watch=True, + ), + 7200.0, + ) - def test_commits_since_are_docs_only_non_docs_path(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.side_effect = [ - mock.MagicMock(returncode=0, stdout="c0mmit1\n"), - mock.MagicMock(returncode=0, stdout="Libraries/PyKotor/src/foo.py\n"), - ] - result = mod._commits_since_are_docs_only("base", "head") - self.assertFalse(result) + def test_last_ci_check_section_extracts_block(self) -> None: + mock_path = mock.MagicMock() + mock_path.is_file.return_value = True + mock_path.read_text.return_value = SAMPLE_DOC + with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): + section = mod._last_ci_check_section() + self.assertIn("26365458400", section) + self.assertIn("26365648344", section) - def test_compare_fc_sha_stale_benign_when_docs_only(self) -> None: - status = { - "verify_pypi": { - "run_id": 26372746392, - "status": "queued", - "conclusion": "", - "head_sha": _MASTER_SHA, - }, - "forward_commits": { - "run_id": 26365648344, - "status": "queued", - "conclusion": "", - "head_sha": _FC_SHA, + def test_apply_lfg_defer_sets_flag_and_stderr(self) -> None: + status: dict[str, Any] = { + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, + "gh_ok": True, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=True): - result = mod._compare_checkpoint(status) - self.assertTrue(result["defer_lfg_pr"]) - self.assertTrue(result["fc_sha_stale"]) - self.assertTrue(result["fc_sha_stale_benign"]) - self.assertIn("docs-only", result.get("fc_sha_stale_note", "")) + with patch("sys.stderr", new_callable=io.StringIO) as err: + deferred = mod._apply_lfg_defer(status, exit_on_defer=True) + self.assertTrue(deferred) + self.assertTrue(status["lfg_deferred"]) + self.assertEqual(status["lfg_defer_reason"], "unchanged_active_runs") + self.assertIn("LFG deferred:", err.getvalue()) + self.assertIn("same canonical runs", err.getvalue()) - def test_compare_no_defer_when_fc_non_docs_stale(self) -> None: + def test_resolve_lfg_defer_reason_fc_active_pending(self) -> None: + checkpoint = { + "defer_lfg_pr": True, + "defer_reason": "FC run still active; classify SHA gap after terminal", + } + self.assertEqual(mod._resolve_lfg_defer_reason(checkpoint), "fc_active_pending") + + def test_compare_pending_note_includes_queued_hours(self) -> None: status = { "verify_pypi": { "run_id": 26372746392, - "status": "queued", - "conclusion": "", + "status": "completed", + "conclusion": "success", "head_sha": _MASTER_SHA, }, "forward_commits": { - "run_id": 26365648344, + "run_id": 26543899770, "status": "queued", "conclusion": "", "head_sha": _FC_SHA, + "queued_hours": 2.5, }, } with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: mock_parse.return_value = { "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, + "forward_commits_run_id": 26543899770, } with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=False): + with patch.object(mod, "_commits_since_are_docs_only", return_value=None): result = mod._compare_checkpoint(status) - self.assertFalse(result["defer_lfg_pr"]) - self.assertIn("non-docs", result.get("defer_reason", "")) + self.assertIn("queued 2.5h", result.get("fc_stale_gap_pending_note", "")) - def test_validate_checkpoint_doc_no_drift(self) -> None: + def test_compute_lfg_exit_reason_deferred_fc_active(self) -> None: + status = {"lfg_defer_reason": "fc_active_pending"} + reason = mod._compute_lfg_exit_reason(status, 2, deferred=True) + self.assertEqual(reason, "deferred:fc_active_pending") + + def test_classify_gh_error_rate_limit(self) -> None: + self.assertEqual( + mod._classify_gh_error_message("HTTP 403: API rate limit exceeded"), + "rate_limited", + ) + + def test_summarize_gh_lookup_rate_limited(self) -> None: status = { - "verify_pypi": {"run_id": 26372746392, "status": "queued", "conclusion": ""}, - "forward_commits": {"run_id": 26365648344, "status": "queued", "conclusion": ""}, + "gh_ok": False, + "verify_pypi": {"error": "HTTP 403: API rate limit exceeded"}, + "forward_commits": {"error": "HTTP 403: API rate limit exceeded"}, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, + summary = mod._summarize_gh_lookup(status) + self.assertIsNotNone(summary) + assert summary is not None + self.assertEqual(summary["primary_kind"], "rate_limited") + self.assertIn("verify:", summary["note"]) + + def test_build_lfg_agent_briefing_gh_unavailable(self) -> None: + briefing = mod._build_lfg_agent_briefing( + { + "gh_ok": False, + "gh_lookup": { + "primary_kind": "rate_limited", + "note": "verify: HTTP 403: API rate limit exceeded", + }, + "doc_checkpoint_snapshot": { + "last_ci_line": "**2026-05-27:** verify success; FC queued", + }, + "proceed_hint": ( + "python3 .github/scripts/local_verify_pypi_slice.py " + "--lfg-preflight # retry when GitHub API rate limit resets" + ), } - with patch.object( - mod, - "_parse_last_ci_check_status_words", - return_value={"verify_status_word": "queued", "fc_status_word": "queued"}, - ): - result = mod._validate_checkpoint_doc(status) - self.assertTrue(result["doc_valid"]) - self.assertEqual(result["drift"], []) - self.assertEqual(result["status_drift"], []) + ) + self.assertEqual(briefing["action"], "gh_unavailable") + self.assertEqual(briefing["reason"], "gh_error:rate_limited") + self.assertEqual(briefing["blocked"], "gh_unavailable") + self.assertIn("rate limit", briefing["notes"][0]) + self.assertTrue(any(note.startswith("doc:") for note in briefing["notes"])) + + def test_lfg_refresh_blocked_gh_unavailable(self) -> None: + status: dict[str, Any] = { + "gh_ok": False, + "checkpoint": {"defer_lfg_pr": False, "proceed_reason": "fix_gh_lookup"}, + } + self.assertEqual(mod._lfg_refresh_blocked(status, deferred=False), "gh_unavailable") + + def test_build_doc_checkpoint_snapshot(self) -> None: + mock_path = mock.MagicMock() + mock_path.is_file.return_value = True + mock_path.read_text.return_value = SAMPLE_DOC + with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): + snapshot = mod._build_doc_checkpoint_snapshot() + self.assertEqual(snapshot["verify_run_id"], 26365458400) + self.assertEqual(snapshot["forward_commits_run_id"], 26365648344) + self.assertIn("26365458400", snapshot["last_ci_line"]) + + def test_ci_status_includes_doc_snapshot_on_gh_failure(self) -> None: + mock_path = mock.MagicMock() + mock_path.is_file.return_value = True + mock_path.read_text.return_value = SAMPLE_DOC + with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): + with patch.object(mod, "_latest_workflow_run") as mock_run: + mock_run.side_effect = [ + {"error": "HTTP 403: API rate limit exceeded"}, + {"error": "HTTP 403: API rate limit exceeded"}, + ] + status = mod._ci_status(compare_checkpoint=True) + self.assertIn("doc_checkpoint_snapshot", status) + self.assertIn("last_ci_line", status["doc_checkpoint_snapshot"]) + + def test_compute_lfg_exit_reason_gh_rate_limited(self) -> None: + status = {"gh_lookup": {"primary_kind": "rate_limited"}} + self.assertEqual( + mod._compute_lfg_exit_reason(status, 1, deferred=False), + "gh_error:rate_limited", + ) - def test_validate_checkpoint_doc_detects_drift(self) -> None: - status = { - "verify_pypi": {"run_id": 999}, - "forward_commits": {"run_id": 26365648344}, - } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, - } - result = mod._validate_checkpoint_doc(status) - self.assertFalse(result["doc_valid"]) - self.assertEqual(len(result["drift"]), 1) + def test_build_proceed_hint_gh_rate_limited(self) -> None: + hint = mod._build_proceed_hint( + { + "gh_ok": False, + "gh_lookup": {"primary_kind": "rate_limited"}, + }, + blocked="gh_unavailable", + ) + self.assertIn("--lfg-preflight", hint) + self.assertIn("rate limit", hint) - def test_git_origin_master_sha_falls_back_to_local_master(self) -> None: - with patch("subprocess.run") as mock_run: + def test_ci_status_attaches_gh_lookup_on_failure(self) -> None: + with patch.object(mod, "_latest_workflow_run") as mock_run: mock_run.side_effect = [ - mock.MagicMock(returncode=1, stdout=""), - mock.MagicMock(returncode=0, stdout="localmaster\n"), + {"error": "HTTP 403: API rate limit exceeded"}, + {"error": "HTTP 403: API rate limit exceeded"}, ] - result = mod._git_origin_master_sha() - self.assertEqual(result, "localmaster") + status = mod._ci_status(compare_checkpoint=True) + self.assertFalse(status["gh_ok"]) + self.assertIn("gh_lookup", status) + self.assertEqual(status["gh_lookup"]["primary_kind"], "rate_limited") - def test_validate_checkpoint_doc_cli(self) -> None: - result = subprocess.run( - [ - sys.executable, - str(SCRIPT_PATH), - "--ci-status-only", - "--validate-checkpoint-doc", - "--json", - ], - capture_output=True, - text=True, - cwd=REPO_ROOT, - check=False, + def test_should_emit_lfg_agent_briefing_stderr_gh_unavailable(self) -> None: + self.assertTrue( + mod._should_emit_lfg_agent_briefing_stderr( + {"action": "gh_unavailable"}, + 0, + ) ) - self.assertIn(result.returncode, (0, 2), msg=result.stderr) - payload = json.loads(result.stdout) - self.assertIn("doc_valid", payload) - def test_ci_status_human_output_does_not_crash(self) -> None: - result = subprocess.run( - [ - sys.executable, - str(SCRIPT_PATH), - "--ci-status-only", - "--compare-checkpoint", - ], - capture_output=True, - text=True, - cwd=REPO_ROOT, - check=False, + def test_build_proceed_hint_fc_active_pending(self) -> None: + hint = mod._build_proceed_hint( + { + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "FC run still active; classify SHA gap after terminal", + } + }, + blocked="deferred", ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("=== CI STATUS ===", result.stdout) + self.assertIn("--lfg-gate-watch", hint) + self.assertIn("terminal", hint) - def test_emit_checkpoint_snippet(self) -> None: - result = subprocess.run( - [ - sys.executable, - str(SCRIPT_PATH), - "--ci-status-only", - "--emit-checkpoint-snippet", - ], - capture_output=True, - text=True, - cwd=REPO_ROOT, - check=False, + def test_build_proceed_hint_investigate_drift_active_fc_gate_watch(self) -> None: + hint = mod._build_proceed_hint( + { + "checkpoint": {"proceed_reason": "investigate_ci_drift"}, + "forward_commits": {"status": "queued", "conclusion": ""}, + "verify_pypi": {"status": "completed", "conclusion": "success"}, + }, + blocked=None, ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("verify [", result.stdout) - self.assertIn("FC [", result.stdout) + self.assertIn("--lfg-gate-watch", hint) - def test_parse_run_ids_from_last_ci_check(self) -> None: - with patch.object(mod, "SOLUTION_CLOSEOUT", Path("/unused")): - with patch.object(mod, "_last_ci_check_section", return_value=SAMPLE_LAST_CHECK): - result = mod._parse_solution_checkpoint_run_ids() - self.assertEqual(result["verify_run_id"], 26365458400) - self.assertEqual(result["forward_commits_run_id"], 26365648344) + def test_primary_watch_command_prefers_gate_watch(self) -> None: + command = mod._primary_watch_command( + { + "preflight_watch": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight-watch --json", + "gate_watch": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", + } + ) + self.assertIn("--lfg-gate-watch", command) - def test_parse_missing_section_returns_error(self) -> None: - with patch.object(mod, "_last_ci_check_section", return_value=""): - with patch.object(mod, "_parse_canonical_table_run_ids", return_value={"error": "no table"}): - result = mod._parse_solution_checkpoint_run_ids() - self.assertIn("error", result) + def test_format_preflight_watch_poll_line_includes_sha_gap(self) -> None: + line = mod._format_preflight_watch_poll_line( + 1, + { + "lfg_defer_reason": "fc_active_pending", + "checkpoint": {"master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f"}, + "forward_commits": { + "run_id": 1, + "status": "queued", + "conclusion": "", + "head_sha": "573c9d4bb474ed3ffdb871d3e081431a51f31702", + }, + }, + ) + self.assertIn("sha_gap=573c9d4:8916e2f", line) + self.assertIn("preflight watch poll", line) - def test_compare_defer_when_queued_and_ids_match(self) -> None: - status = { - "verify_pypi": { - "run_id": 26365458400, - "status": "queued", - "conclusion": "", - "head_sha": _STALE_VERIFY_SHA, + def test_format_deferred_watch_poll_line_sha_gap_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "checkpoint": { + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", }, "forward_commits": { - "run_id": 26365648344, + "run_id": 1, "status": "queued", "conclusion": "", - "head_sha": _STALE_VERIFY_SHA, + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "queued_hours": 0.1, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26365458400, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_STALE_VERIFY_SHA): - result = mod._compare_checkpoint(status) - self.assertTrue(result["defer_lfg_pr"]) - self.assertTrue(result["checkpoint_unchanged"]) + line = mod._format_preflight_watch_poll_line(1, status) + tokens = line.split() + self.assertIn("sha_gap=7d85438:8916e2f", tokens) + self.assertEqual(sum(1 for token in tokens if token.startswith("sha_gap=")), 1) - def test_compare_defer_when_in_progress_and_ids_match(self) -> None: - status = { - "verify_pypi": { - "run_id": 26365458400, - "status": "in_progress", - "conclusion": "", - "head_sha": "abc123", + def test_format_gate_watch_poll_line_sha_gap_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "fc_active_pending", + "checkpoint": { + "fc_sha_stale": True, + "master_sha": "8916e2ffe1b57169693b2c9d9ea2b63eeb7fed8f", }, "forward_commits": { - "run_id": 26365648344, + "run_id": 1, "status": "queued", "conclusion": "", - "head_sha": "abc123", + "head_sha": "7d85438b090178c8c8924abc46565f7c6ded19", + "queued_hours": 0.1, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26365458400, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): - result = mod._compare_checkpoint(status) - self.assertTrue(result["defer_lfg_pr"]) + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + tokens = line.split() + self.assertIn("gate watch poll", line) + self.assertIn("sha_gap=7d85438:8916e2f", tokens) + self.assertEqual(sum(1 for token in tokens if token.startswith("sha_gap=")), 1) + + def test_format_gate_watch_poll_line_label(self) -> None: + line = mod._format_preflight_watch_poll_line( + 2, + {"lfg_defer_reason": "fc_active_pending"}, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertNotIn("preflight watch poll", line) - def test_compare_no_defer_when_verify_sha_stale(self) -> None: - status = { + def test_format_preflight_watch_poll_line_gh_watch(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, "verify_pypi": { - "run_id": 26365458400, + "run_id": 1, + "url": "https://example.com/runs/1", "status": "queued", "conclusion": "", - "head_sha": _STALE_VERIFY_SHA, + "queued_hours": 1.5, }, "forward_commits": { - "run_id": 26365648344, + "run_id": 2, + "url": "https://example.com/runs/2", "status": "queued", "conclusion": "", - "head_sha": _FC_SHA, + "queued_hours": 1.0, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26365458400, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - result = mod._compare_checkpoint(status) - self.assertFalse(result["defer_lfg_pr"]) - self.assertTrue(result["verify_sha_stale"]) - self.assertIn("workflow_dispatch", result.get("recommended_action", "")) - - def test_compare_defer_when_fc_active_verify_completed_same_sha(self) -> None: - status = { - "verify_pypi": { - "run_id": 26365458400, - "status": "completed", - "conclusion": "success", - "head_sha": "abc123", - }, - "forward_commits": { - "run_id": 26365648344, - "status": "queued", - "conclusion": "", - "head_sha": "abc123", + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("gh_watch=verify:1,fc:2", line) + self.assertIn("verify_run_url=https://example.com/runs/1", line) + self.assertIn("fc_run_url=https://example.com/runs/2", line) + self.assertEqual(line.count("gh_watch=verify:1,fc:2"), 1) + tokens = line.split() + self.assertIn("active_runs=verify,fc", tokens) + self.assertEqual(sum(1 for token in tokens if token == "active_runs=verify,fc"), 1) + self.assertIn("queued=1.5h", line) + tokens = line.split() + self.assertEqual(sum(1 for token in tokens if token == "queued=1.5h"), 1) + self.assertNotIn("verify_queued=1.5h", tokens) + self.assertNotIn("fc_queued=1.0h", tokens) + self.assertIn("expected_after=closeout", line) + self.assertIn("primary_action=gate_watch", line) + self.assertIn("watch_recommended=true", line) + self.assertIn("watch=gh run watch 2 --exit-status", line) + self.assertIn("briefing_command=", line) + self.assertIn("--lfg-gate-watch", line) + + def test_format_gate_watch_poll_line_primary_action_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, - } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26365458400, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): - result = mod._compare_checkpoint(status) - self.assertTrue(result["defer_lfg_pr"]) - self.assertIn("fc_active_closeout_note", result) - - def test_compare_no_defer_on_run_id_drift(self) -> None: - status = { "verify_pypi": { - "run_id": 99999999999, + "run_id": 1, "status": "queued", "conclusion": "", - "head_sha": "abc123", + "queued_hours": 1.5, }, "forward_commits": { - "run_id": 26365648344, + "run_id": 2, "status": "queued", "conclusion": "", - "head_sha": "abc123", + "queued_hours": 1.0, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26365458400, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value="abc123"): - result = mod._compare_checkpoint(status) - self.assertFalse(result["defer_lfg_pr"]) - self.assertEqual(result.get("proceed_reason"), "investigate_ci_drift") - self.assertIn("ci_drift_note", result) - - def test_compare_investigate_drift_before_fc_classify_gap(self) -> None: - status = { + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + tokens = line.split() + self.assertIn("gate watch poll", line) + self.assertIn("primary_action=gate_watch", tokens) + self.assertIn("expected_after=closeout", tokens) + self.assertEqual(sum(1 for token in tokens if token == "primary_action=gate_watch"), 1) + self.assertEqual(sum(1 for token in tokens if token == "expected_after=closeout"), 1) + + def test_format_deferred_watch_poll_line_watch_commands_top_level(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, "verify_pypi": { - "run_id": 26372746392, - "status": "completed", - "conclusion": "success", - "head_sha": _MASTER_SHA, + "run_id": 1, + "status": "queued", + "conclusion": "", + "queued_hours": 1.5, }, "forward_commits": { - "run_id": 26543899770, + "run_id": 2, "status": "queued", "conclusion": "", - "head_sha": _FC_SHA, + "queued_hours": 1.0, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26365648344, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=None): - result = mod._compare_checkpoint(status) - self.assertEqual(result.get("proceed_reason"), "investigate_ci_drift") - self.assertIn("26543899770", result.get("ci_drift_note", "")) - - def test_compare_defer_classify_gap_when_fc_active(self) -> None: - status = { + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("watch=gh run watch 2 --exit-status", line) + self.assertEqual(line.count("watch=gh run watch 2 --exit-status"), 1) + self.assertIn("briefing_command=", line) + self.assertIn("--lfg-gate-watch", line) + + def test_format_gate_watch_poll_line_watch_commands_top_level(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, "verify_pypi": { - "run_id": 26372746392, - "status": "completed", - "conclusion": "success", - "head_sha": _MASTER_SHA, + "run_id": 1, + "status": "queued", + "conclusion": "", + "queued_hours": 1.5, }, "forward_commits": { - "run_id": 26543899770, + "run_id": 2, "status": "queued", "conclusion": "", - "head_sha": _FC_SHA, + "queued_hours": 1.0, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26543899770, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=None): - result = mod._compare_checkpoint(status) - self.assertTrue(result.get("defer_lfg_pr")) - self.assertNotIn("proceed_reason", result) - self.assertIn("fc_stale_gap_pending_note", result) - self.assertIn("queued", result.get("fc_stale_gap_pending_note", "")) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("watch=gh run watch 2 --exit-status", line) + self.assertIn("briefing_command=", line) + self.assertIn("--lfg-gate-watch", line) - def test_compare_classify_gap_when_fc_terminal_benign_unknown(self) -> None: - status = { + def test_format_gate_watch_poll_line_active_runs_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, "verify_pypi": { - "run_id": 26372746392, - "status": "completed", - "conclusion": "success", - "head_sha": _MASTER_SHA, + "run_id": 1, + "url": "https://example.com/runs/1", + "status": "queued", + "conclusion": "", + "queued_hours": 1.5, }, "forward_commits": { - "run_id": 26543899770, - "status": "completed", - "conclusion": "success", - "head_sha": _FC_SHA, + "run_id": 2, + "url": "https://example.com/runs/2", + "status": "queued", + "conclusion": "", + "queued_hours": 1.0, }, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26543899770, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=None): - result = mod._compare_checkpoint(status) - self.assertFalse(result.get("defer_lfg_pr")) - self.assertEqual(result.get("proceed_reason"), "classify_fc_stale_gap") - self.assertIn("fc_stale_gap_note", result) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + tokens = line.split() + self.assertIn("gate watch poll", line) + self.assertIn("active_runs=verify,fc", tokens) + self.assertEqual(sum(1 for token in tokens if token == "active_runs=verify,fc"), 1) + self.assertIn("verify_run_url=https://example.com/runs/1", line) + self.assertIn("fc_run_url=https://example.com/runs/2", line) + + def test_format_preflight_watch_poll_line_queue_note(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + "queue_backlog_note": "verify queued 5.2h; FC queued 5.3h", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 5.2}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 5.3}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("queue_note=verify queued 5.2h", line) - def test_build_lfg_agent_briefing_defer_fc_active_pending(self) -> None: - briefing = mod._build_lfg_agent_briefing( - { - "lfg_deferred": True, - "lfg_defer_reason": "fc_active_pending", - "proceed_hint": ( - "python3 .github/scripts/local_verify_pypi_slice.py " - "--lfg-preflight # re-check when FC run reaches terminal" - ), - "checkpoint": { - "fc_stale_gap_pending_note": "FC queued on def1234 vs master abc1234", - }, - "forward_commits": { - "run_id": 26546235822, - "status": "queued", - "conclusion": "", - "url": "https://example.com/runs/26546235822", - }, - } - ) - self.assertEqual(briefing["action"], "defer") - self.assertEqual(briefing["reason"], "fc_active_pending") - self.assertIn("FC queued", briefing["notes"][0]) - self.assertEqual(briefing["fc_run_id"], 26546235822) - self.assertEqual(briefing["fc_run_url"], "https://example.com/runs/26546235822") - self.assertEqual(briefing["fc_status"], "queued") - monitor = briefing["monitor_commands"] - self.assertIn("preflight_retry", monitor) - self.assertEqual( - monitor["watch_fc_run"], - "gh run watch 26546235822 --exit-status", - ) - self.assertIn("preflight_watch", monitor) - self.assertIn("--lfg-preflight-watch", monitor["preflight_watch"]) + def test_format_gate_watch_poll_line_queue_note(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + "queue_backlog_note": "Runner backlog ~3h", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 2.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("queue_note=Runner backlog ~3h", line) - def test_build_defer_monitor_commands_verify_active(self) -> None: - commands = mod._build_defer_monitor_commands( - { - "command": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-preflight", - "verify_run_id": 26372746392, - } - ) - self.assertEqual( - commands["watch_verify_run"], - "gh run watch 26372746392 --exit-status", - ) + def test_format_preflight_watch_poll_line_blocked(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("blocked=deferred", line) - def test_emit_defer_briefing_stderr_includes_reason_and_fc_run(self) -> None: - with patch.object(mod.sys, "stderr", new_callable=io.StringIO) as err: - mod._emit_lfg_agent_briefing_stderr( - { - "action": "defer", - "reason": "fc_active_pending", - "blocked": "deferred", - "fc_run_id": 26546235822, - "monitor_commands": { - "watch_fc_run": "gh run watch 26546235822 --exit-status", - }, - } + def test_format_gate_watch_poll_line_blocked(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", ) - output = err.getvalue() - self.assertIn("reason=fc_active_pending", output) - self.assertIn("fc_run=26546235822", output) - self.assertIn("watch=gh run watch 26546235822 --exit-status", output) + self.assertIn("gate watch poll", line) + self.assertIn("blocked=deferred", line) - def test_watch_lfg_preflight_defer_proceed(self) -> None: - deferred_status = { - "gh_ok": True, - "checkpoint": {"defer_lfg_pr": True, "defer_reason": "FC run still active"}, - "forward_commits": { - "run_id": 1, - "status": "queued", - "conclusion": "", + def test_format_preflight_watch_poll_line_briefing_reason(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - proceed_status = { - "gh_ok": True, + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("briefing_reason=unchanged_active_runs", line) + + def test_format_gate_watch_poll_line_briefing_reason(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", "checkpoint": { - "defer_lfg_pr": False, - "proceed_reason": "update_monitoring_docs", - }, - "forward_commits": { - "run_id": 1, - "status": "completed", - "conclusion": "success", + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - with patch.object(mod, "_ci_status", side_effect=[deferred_status, proceed_status]): - with patch.object(mod, "_refine_lfg_checkpoint"): - with patch.object(mod.time, "sleep"): - status = mod._watch_lfg_preflight_defer( - targets=["solution", "plan020"], - prefetch_git=False, - interval_sec=0.0, - timeout_sec=60.0, - ) - self.assertEqual(status["lfg_preflight_watch_result"], "proceed") - self.assertFalse(status.get("lfg_deferred")) - summary = status.get("preflight_watch_summary") or {} - self.assertEqual(summary.get("polls"), 2) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("briefing_reason=unchanged_active_runs", line) - def test_watch_lfg_preflight_defer_timeout(self) -> None: - deferred_status = { - "gh_ok": True, - "checkpoint": {"defer_lfg_pr": True}, - "forward_commits": {"run_id": 1, "status": "queued", "conclusion": ""}, + def test_format_preflight_watch_poll_line_briefing_action(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - with patch.object(mod, "_ci_status", return_value=deferred_status): - with patch.object(mod, "_refine_lfg_checkpoint"): - with patch.object(mod.time, "sleep"): - with patch.object(mod.time, "monotonic", side_effect=[0.0, 0.0, 100.0]): - status = mod._watch_lfg_preflight_defer( - targets=["solution"], - prefetch_git=False, - interval_sec=0.0, - timeout_sec=5.0, - ) - self.assertEqual(status["lfg_preflight_watch_result"], "timeout") - self.assertTrue(status.get("lfg_deferred")) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("action=defer", line) - def test_resolve_lfg_mode_preflight_watch(self) -> None: - self.assertEqual( - mod._resolve_lfg_mode( - lfg_merge_watch=False, - lfg_merge_gate=False, - lfg_closeout=False, - lfg_gate=False, - lfg_preflight=True, - lfg_preflight_watch=True, - lfg_refresh=True, - lfg_pr_watch=False, - dry_run=True, - ), - "preflight_watch", - ) + def test_format_gate_watch_poll_line_briefing_action(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("action=defer", line) - def test_resolve_watch_timeout_preflight_watch(self) -> None: - self.assertEqual( - mod._resolve_watch_timeout_seconds( - None, - lfg_merge_watch=False, - lfg_preflight_watch=True, - ), - 7200.0, - ) + def test_format_preflight_watch_poll_line_briefing_notes(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + "queue_backlog_note": "Runner backlog ~3h", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("notes=1", line) - def test_last_ci_check_section_extracts_block(self) -> None: - mock_path = mock.MagicMock() - mock_path.is_file.return_value = True - mock_path.read_text.return_value = SAMPLE_DOC - with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): - section = mod._last_ci_check_section() - self.assertIn("26365458400", section) - self.assertIn("26365648344", section) + def test_format_gate_watch_poll_line_briefing_notes(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + "queue_backlog_note": "Runner backlog ~3h", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("notes=1", line) - def test_apply_lfg_defer_sets_flag_and_stderr(self) -> None: + def test_format_preflight_watch_poll_line_merge_ready(self) -> None: status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", "checkpoint": { "defer_lfg_pr": True, "defer_reason": "same canonical runs still active on unchanged checkpoint", }, - "gh_ok": True, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - with patch("sys.stderr", new_callable=io.StringIO) as err: - deferred = mod._apply_lfg_defer(status, exit_on_defer=True) - self.assertTrue(deferred) - self.assertTrue(status["lfg_deferred"]) - self.assertEqual(status["lfg_defer_reason"], "unchanged_active_runs") - self.assertIn("LFG deferred:", err.getvalue()) - self.assertIn("same canonical runs", err.getvalue()) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertIn("merge_ready=false", line) - def test_resolve_lfg_defer_reason_fc_active_pending(self) -> None: - checkpoint = { - "defer_lfg_pr": True, - "defer_reason": "FC run still active; classify SHA gap after terminal", + def test_format_gate_watch_poll_line_merge_ready(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - self.assertEqual(mod._resolve_lfg_defer_reason(checkpoint), "fc_active_pending") + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertIn("merge_ready=false", line) - def test_compare_pending_note_includes_queued_hours(self) -> None: - status = { - "verify_pypi": { - "run_id": 26372746392, - "status": "completed", - "conclusion": "success", - "head_sha": _MASTER_SHA, + def test_format_preflight_watch_poll_line_run_ids(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, - "forward_commits": { - "run_id": 26543899770, - "status": "queued", - "conclusion": "", - "head_sha": _FC_SHA, - "queued_hours": 2.5, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + tokens = line.split() + self.assertIn("verify_run=1", tokens) + self.assertIn("fc_run=2", tokens) + self.assertNotIn("verify=1", tokens) + self.assertNotIn("fc=2", tokens) + + def test_format_gate_watch_poll_line_run_ids(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - with patch.object(mod, "_parse_solution_checkpoint_run_ids") as mock_parse: - mock_parse.return_value = { - "verify_run_id": 26372746392, - "forward_commits_run_id": 26543899770, - } - with patch.object(mod, "_git_origin_master_sha", return_value=_MASTER_SHA): - with patch.object(mod, "_commits_since_are_docs_only", return_value=None): - result = mod._compare_checkpoint(status) - self.assertIn("queued 2.5h", result.get("fc_stale_gap_pending_note", "")) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + tokens = line.split() + self.assertIn("verify_run=1", tokens) + self.assertIn("fc_run=2", tokens) + self.assertNotIn("verify=1", tokens) + self.assertNotIn("fc=2", tokens) + + def test_format_preflight_watch_poll_line_legacy_run_ids_when_not_deferred(self) -> None: + status: dict[str, Any] = { + "lfg_defer_reason": "unchanged_active_runs", + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + line = mod._format_preflight_watch_poll_line(1, status) + tokens = line.split() + self.assertIn("verify=1", tokens) + self.assertIn("fc=2", tokens) + self.assertNotIn("verify_run=1", tokens) + self.assertNotIn("fc_run=2", tokens) + + def test_format_preflight_watch_poll_line_per_run_queued_when_not_deferred(self) -> None: + status: dict[str, Any] = { + "lfg_defer_reason": "unchanged_active_runs", + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + line = mod._format_preflight_watch_poll_line(1, status) + tokens = line.split() + self.assertIn("verify_queued=1.5h", tokens) + self.assertIn("fc_queued=1.0h", tokens) - def test_compute_lfg_exit_reason_deferred_fc_active(self) -> None: - status = {"lfg_defer_reason": "fc_active_pending"} - reason = mod._compute_lfg_exit_reason(status, 2, deferred=True) - self.assertEqual(reason, "deferred:fc_active_pending") + def test_format_preflight_watch_poll_line_run_status_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + self.assertEqual(line.count("verify_status=queued"), 1) + self.assertEqual(line.count("fc_status=queued"), 1) - def test_classify_gh_error_rate_limit(self) -> None: - self.assertEqual( - mod._classify_gh_error_message("HTTP 403: API rate limit exceeded"), - "rate_limited", - ) + def test_format_gate_watch_poll_line_run_status_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertEqual(line.count("verify_status=queued"), 1) + self.assertEqual(line.count("fc_status=queued"), 1) - def test_summarize_gh_lookup_rate_limited(self) -> None: - status = { - "gh_ok": False, - "verify_pypi": {"error": "HTTP 403: API rate limit exceeded"}, - "forward_commits": {"error": "HTTP 403: API rate limit exceeded"}, + def test_format_gate_watch_poll_line_gh_watch_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 1.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, } - summary = mod._summarize_gh_lookup(status) - self.assertIsNotNone(summary) - assert summary is not None - self.assertEqual(summary["primary_kind"], "rate_limited") - self.assertIn("verify:", summary["note"]) + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + self.assertIn("gate watch poll", line) + self.assertEqual(line.count("gh_watch=verify:1,fc:2"), 1) - def test_build_lfg_agent_briefing_gh_unavailable(self) -> None: - briefing = mod._build_lfg_agent_briefing( + def test_format_preflight_watch_poll_line_queue_warn(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 2.5}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 1.0}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line(1, status) + tokens = line.split() + self.assertIn("queued=2.5h", tokens) + self.assertEqual(sum(1 for token in tokens if token == "queued=2.5h"), 1) + self.assertIn("queue_warn=true", tokens) + self.assertEqual(sum(1 for token in tokens if token == "queue_warn=true"), 1) + self.assertNotIn("verify_queued=2.5h", tokens) + self.assertNotIn("fc_queued=1.0h", tokens) + + def test_format_gate_watch_poll_line_queue_backlog_once(self) -> None: + status: dict[str, Any] = { + "lfg_deferred": True, + "lfg_defer_reason": "unchanged_active_runs", + "checkpoint": { + "defer_lfg_pr": True, + "defer_reason": "same canonical runs still active on unchanged checkpoint", + }, + "verify_pypi": {"run_id": 1, "status": "queued", "conclusion": "", "queued_hours": 5.2}, + "forward_commits": {"run_id": 2, "status": "queued", "conclusion": "", "queued_hours": 5.3}, + } + with patch.object(mod, "_defer_preflight_watch_recommended", return_value=True): + line = mod._format_preflight_watch_poll_line( + 2, + status, + watch_label="gate", + ) + tokens = line.split() + self.assertIn("gate watch poll", line) + self.assertIn("queue_backlog=true", tokens) + self.assertEqual(sum(1 for token in tokens if token == "queue_backlog=true"), 1) + self.assertIn("queued=5.3h", tokens) + self.assertEqual(sum(1 for token in tokens if token == "queued=5.3h"), 1) + + def test_format_preflight_watch_summary_line_includes_next_hint(self) -> None: + line = mod._format_preflight_watch_summary_line( { - "gh_ok": False, - "gh_lookup": { - "primary_kind": "rate_limited", - "note": "verify: HTTP 403: API rate limit exceeded", - }, - "doc_checkpoint_snapshot": { - "last_ci_line": "**2026-05-27:** verify success; FC queued", - }, - "proceed_hint": ( - "python3 .github/scripts/local_verify_pypi_slice.py " - "--lfg-preflight # retry when GitHub API rate limit resets" - ), + "lfg_preflight_watch_result": "timeout", + "polls": 3, + "watch_duration_sec": 12.0, + "next_hint": "python3 .github/scripts/local_verify_pypi_slice.py --lfg-gate-watch --json", } ) - self.assertEqual(briefing["action"], "gh_unavailable") - self.assertEqual(briefing["reason"], "gh_error:rate_limited") - self.assertEqual(briefing["blocked"], "gh_unavailable") - self.assertIn("rate limit", briefing["notes"][0]) - self.assertTrue(any(note.startswith("doc:") for note in briefing["notes"])) + self.assertIn("result=timeout", line) + self.assertIn("next=", line) + self.assertIn("--lfg-gate-watch", line) - def test_lfg_refresh_blocked_gh_unavailable(self) -> None: - status: dict[str, Any] = { - "gh_ok": False, - "checkpoint": {"defer_lfg_pr": False, "proceed_reason": "fix_gh_lookup"}, - } - self.assertEqual(mod._lfg_refresh_blocked(status, deferred=False), "gh_unavailable") + def test_format_preflight_watch_summary_line_reason_transition(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "proceed", + "polls": 4, + "watch_duration_sec": 30.0, + "start_defer_reason": "fc_active_pending", + "end_defer_reason": "investigate_ci_drift", + } + ) + self.assertIn("reason=fc_active_pending->investigate_ci_drift", line) - def test_build_doc_checkpoint_snapshot(self) -> None: - mock_path = mock.MagicMock() - mock_path.is_file.return_value = True - mock_path.read_text.return_value = SAMPLE_DOC - with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): - snapshot = mod._build_doc_checkpoint_snapshot() - self.assertEqual(snapshot["verify_run_id"], 26365458400) - self.assertEqual(snapshot["forward_commits_run_id"], 26365648344) - self.assertIn("26365458400", snapshot["last_ci_line"]) + def test_format_preflight_watch_summary_line_active_runs(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "active_runs": ["verify", "fc"], + } + ) + self.assertIn("active_runs=verify,fc", line) - def test_ci_status_includes_doc_snapshot_on_gh_failure(self) -> None: - mock_path = mock.MagicMock() - mock_path.is_file.return_value = True - mock_path.read_text.return_value = SAMPLE_DOC - with patch.object(mod, "SOLUTION_CLOSEOUT", mock_path): - with patch.object(mod, "_latest_workflow_run") as mock_run: - mock_run.side_effect = [ - {"error": "HTTP 403: API rate limit exceeded"}, - {"error": "HTTP 403: API rate limit exceeded"}, - ] - status = mod._ci_status(compare_checkpoint=True) - self.assertIn("doc_checkpoint_snapshot", status) - self.assertIn("last_ci_line", status["doc_checkpoint_snapshot"]) + def test_format_preflight_watch_summary_line_gh_watch(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "gh_watch_summary": "verify:1,fc:2", + } + ) + self.assertIn("gh_watch=verify:1,fc:2", line) - def test_compute_lfg_exit_reason_gh_rate_limited(self) -> None: - status = {"gh_lookup": {"primary_kind": "rate_limited"}} - self.assertEqual( - mod._compute_lfg_exit_reason(status, 1, deferred=False), - "gh_error:rate_limited", + def test_format_preflight_watch_summary_line_run_refs(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "verify_run_id": 1, + "fc_run_id": 2, + "verify_run_url": "https://example.com/runs/1", + "fc_run_url": "https://example.com/runs/2", + } ) + self.assertIn("verify_run=1", line) + self.assertIn("fc_run=2", line) + self.assertIn("verify_run_url=https://example.com/runs/1", line) + self.assertIn("fc_run_url=https://example.com/runs/2", line) - def test_build_proceed_hint_gh_rate_limited(self) -> None: - hint = mod._build_proceed_hint( + def test_format_gate_watch_summary_line_run_refs(self) -> None: + line = mod._format_preflight_watch_summary_line( { - "gh_ok": False, - "gh_lookup": {"primary_kind": "rate_limited"}, + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "verify_run_id": 1, + "fc_run_id": 2, + "verify_run_url": "https://example.com/runs/1", + "fc_run_url": "https://example.com/runs/2", }, - blocked="gh_unavailable", + watch_label="gate", ) - self.assertIn("--lfg-preflight", hint) - self.assertIn("rate limit", hint) + self.assertIn("verify_run=1", line) + self.assertIn("fc_run=2", line) - def test_ci_status_attaches_gh_lookup_on_failure(self) -> None: - with patch.object(mod, "_latest_workflow_run") as mock_run: - mock_run.side_effect = [ - {"error": "HTTP 403: API rate limit exceeded"}, - {"error": "HTTP 403: API rate limit exceeded"}, - ] - status = mod._ci_status(compare_checkpoint=True) - self.assertFalse(status["gh_ok"]) - self.assertIn("gh_lookup", status) - self.assertEqual(status["gh_lookup"]["primary_kind"], "rate_limited") + def test_format_preflight_watch_summary_line_queued(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "max_queued_hours": 1.5, + "queue_backlog_warning": True, + } + ) + self.assertIn("queued=1.5h", line) + self.assertIn("queue_warn=true", line) - def test_should_emit_lfg_agent_briefing_stderr_gh_unavailable(self) -> None: - self.assertTrue( - mod._should_emit_lfg_agent_briefing_stderr( - {"action": "gh_unavailable"}, - 0, - ) + def test_format_preflight_watch_summary_line_queued_prefers_top_level(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "max_queued_hours": 2.5, + "queue_backlog_warning": True, + "queue_context": { + "max_queued_hours": 1.0, + "queue_backlog_severe": True, + }, + } ) + self.assertIn("queued=2.5h", line) + self.assertIn("queue_warn=true", line) + self.assertNotIn("queue_backlog=true", line) - def test_build_proceed_hint_fc_active_pending(self) -> None: - hint = mod._build_proceed_hint( + def test_format_preflight_watch_summary_line_queued_queue_context_fallback(self) -> None: + line = mod._format_preflight_watch_summary_line( { - "checkpoint": { - "defer_lfg_pr": True, - "defer_reason": "FC run still active; classify SHA gap after terminal", - } - }, - blocked="deferred", + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "queue_context": { + "max_queued_hours": 1.5, + "queue_backlog_warning": True, + }, + } ) - self.assertIn("--lfg-preflight", hint) - self.assertIn("terminal", hint) + self.assertIn("queued=1.5h", line) + self.assertIn("queue_warn=true", line) + + def test_format_preflight_watch_summary_line_expected_after(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "expected_after_terminal": {"action": "closeout"}, + "primary_action": "gate_watch", + } + ) + self.assertIn("expected_after=closeout", line) + self.assertIn("primary_action=gate_watch", line) + + def test_format_preflight_watch_summary_line_watch_recommended(self) -> None: + line = mod._format_preflight_watch_summary_line( + { + "lfg_preflight_watch_result": "timeout", + "polls": 2, + "watch_duration_sec": 5.0, + "watch_recommended": True, + } + ) + self.assertIn("watch_recommended=true", line) def test_apply_lfg_defer_skipped_when_disabled(self) -> None: status: dict[str, Any] = {"checkpoint": {"defer_lfg_pr": True}} @@ -3028,6 +5763,7 @@ def test_resolve_lfg_mode_closeout(self) -> None: lfg_merge_gate=False, lfg_closeout=True, lfg_gate=False, + lfg_gate_watch=False, lfg_preflight=False, lfg_preflight_watch=False, lfg_refresh=True, @@ -3042,6 +5778,7 @@ def test_resolve_lfg_mode_closeout(self) -> None: lfg_merge_gate=False, lfg_closeout=False, lfg_gate=True, + lfg_gate_watch=False, lfg_preflight=True, lfg_preflight_watch=False, lfg_refresh=True, @@ -3056,6 +5793,7 @@ def test_resolve_lfg_mode_closeout(self) -> None: lfg_merge_gate=True, lfg_closeout=False, lfg_gate=True, + lfg_gate_watch=False, lfg_preflight=True, lfg_preflight_watch=False, lfg_refresh=True, @@ -3070,6 +5808,7 @@ def test_resolve_lfg_mode_closeout(self) -> None: lfg_merge_gate=False, lfg_closeout=False, lfg_gate=True, + lfg_gate_watch=False, lfg_preflight=True, lfg_preflight_watch=False, lfg_refresh=True, @@ -3084,6 +5823,7 @@ def test_resolve_lfg_mode_closeout(self) -> None: lfg_merge_gate=True, lfg_closeout=False, lfg_gate=True, + lfg_gate_watch=False, lfg_preflight=True, lfg_preflight_watch=False, lfg_refresh=True, diff --git a/docs/plans/2026-05-24-020-verify-pypi-regression-post-268-plan.md b/docs/plans/2026-05-24-020-verify-pypi-regression-post-268-plan.md index 8b9f08084..b3e15a920 100644 --- a/docs/plans/2026-05-24-020-verify-pypi-regression-post-268-plan.md +++ b/docs/plans/2026-05-24-020-verify-pypi-regression-post-268-plan.md @@ -40,8 +40,8 @@ Plan 019 landed via PR #268 but remained `in_progress` without post-merge verifi | Stale branch cleanup | `fix/pypi-verify-regression-concurrency` deleted (merged #275, stray docs) | ✅ plan 026 | | Local CLI PyPI parity (plan 042) | holopatcher/kotormcp install from PyPI; kotordiff not on PyPI; `--help` rc=1 (workflow continue-on-error) | ✅ pass (parity with CI skip semantics; py3.14 local) | | Local PyPI parity (plan 041) | ephemeral venv `pip install pykotor[all]` + workflow import scripts | ✅ pass (Linux/py3; CI matrix still queued) | -| Verify PyPI CI (post-#277) | https://github.com/OpenKotOR/PyKotor/actions/runs/26372746392 | ✅ success — **Check trigger** on `8916e2f`| -| Forward Commits (post-#306) | https://github.com/OpenKotOR/PyKotor/actions/runs/26547345351 | ⏳ pending — merge on `44ccf2a`| +| Verify PyPI CI (post-#277) | https://github.com/OpenKotOR/PyKotor/actions/runs/26549547772 | ✅ success — **Check trigger** on `ca61ce8`| +| Forward Commits (post-#306) | https://github.com/OpenKotOR/PyKotor/actions/runs/26549293445 | ❌ failure — merge on `ca61ce8`| | Local FC dry-run (plan 051) | cherry-pick `49da28057`→bleeding-edge + workflow restore | ✅ pass (`d8dc53968`; docs conflict auto-resolved) | | Solution doc (plan 050) | `docs/solutions/testing/verify-pypi-regression-closeout.md` | ✅ prefer/defer/avoid + local command | | Local verify script (plan 048) | `python3 .github/scripts/local_verify_pypi_slice.py` | ✅ pass (replaces manual plan 047 slice) | @@ -62,9 +62,9 @@ Plan 019 landed via PR #268 but remained `in_progress` without post-merge verifi **Track status (plan 051):** **Monitoring-only.** Code and local parity complete (#268–#306, plans 019–066). Await CI green on [26372746392](https://github.com/OpenKotOR/PyKotor/actions/runs/26372746392) and FC [26365648344](https://github.com/OpenKotOR/PyKotor/actions/runs/26365648344); no further workflow changes unless CI reports new failures. -**Last CI check (plan 114):** 2026-05-27 — verify [26372746392](https://github.com/OpenKotOR/PyKotor/actions/runs/26372746392) success on `8916e2f`; FC [26547345351](https://github.com/OpenKotOR/PyKotor/actions/runs/26547345351) pending on `44ccf2a`. +**Last CI check (plan 214):** 2026-05-29 — verify [26549547772](https://github.com/OpenKotOR/PyKotor/actions/runs/26549547772) success on `ca61ce8`; FC [26549293445](https://github.com/OpenKotOR/PyKotor/actions/runs/26549293445) failure on `ca61ce8`. -**Plans:** 019–114 document the closeout track; authoritative learning in `docs/solutions/testing/verify-pypi-regression-closeout.md`. +**Plans:** 019–214 document the closeout track; authoritative learning in `docs/solutions/testing/verify-pypi-regression-closeout.md`. --- diff --git a/docs/plans/2026-05-24-115-investigate-drift-active-wait-plan.md b/docs/plans/2026-05-24-115-investigate-drift-active-wait-plan.md new file mode 100644 index 000000000..6752c516b --- /dev/null +++ b/docs/plans/2026-05-24-115-investigate-drift-active-wait-plan.md @@ -0,0 +1,37 @@ +--- +title: "fix: investigate ci drift briefing wait on active runs" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Investigate CI Drift Active-Run Wait (plan 115) + +## Summary + +When `proceed_reason` is `investigate_ci_drift` but FC/verify runs are still active, agents should wait before `--lfg-refresh --dry-run`. Enrich drift briefing with structured fields and route to `--lfg-preflight-watch`. + +--- + +## Problem Frame + +Live: FC run IDs churn while queued; drift briefing only has text notes and suggests refresh dry-run prematurely. + +--- + +## Requirements + +- R1. `_build_ci_drift_detail` exposes doc vs live drift fields and `wait_recommended`. +- R2. `_build_drift_refresh_commands` lists refresh/closeout/preflight-watch commands. +- R3. `investigate_ci_drift` briefing includes `drift`, `refresh_commands`, active run refs, `monitor_commands` when waiting. +- R4. `_build_proceed_hint` prefers `--lfg-preflight-watch` when drift + active runs. +- R5. stderr `wait=true` and drift field hints; tests; `PLAN_TRACK_CAP` `115`; docs. + +--- + +## Test scenarios + +- T1. Drift + FC queued → `wait_recommended` true, command includes preflight-watch. +- T2. Drift + both terminal → `refresh_commands.closeout` present. +- T3. stderr includes `wait=true` when active. diff --git a/docs/plans/2026-05-24-116-defer-preflight-watch-command-plan.md b/docs/plans/2026-05-24-116-defer-preflight-watch-command-plan.md new file mode 100644 index 000000000..145bb9434 --- /dev/null +++ b/docs/plans/2026-05-24-116-defer-preflight-watch-command-plan.md @@ -0,0 +1,36 @@ +--- +title: "fix: defer fc-active command routes to preflight-watch" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Defer Preflight-Watch Command (plan 116) + +## Summary + +When deferred on active FC (`fc_active_pending`), `monitor_commands` includes `preflight_watch` but `command`/`proceed_hint` still suggest manual `--lfg-preflight` retry. Route primary command to **`--lfg-preflight-watch`**. + +--- + +## Problem Frame + +Live: `fc_active_pending`; agents get watch_fc_run but command says re-run preflight manually. + +--- + +## Requirements + +- R1. `_defer_preflight_watch_recommended` true for fc/verify active defer reasons. +- R2. `_build_proceed_hint` uses `--lfg-preflight-watch` for those defers. +- R3. Defer briefing sets `watch_recommended` and `command` to preflight-watch. +- R4. Defer stderr includes `watch_recommended=true`; tests; `PLAN_TRACK_CAP` `116`; docs. + +--- + +## Test scenarios + +- T1. fc_active_pending proceed_hint → preflight-watch. +- T2. Defer briefing command matches preflight_watch monitor command. +- T3. stderr includes watch_recommended=true. diff --git a/docs/plans/2026-05-24-117-defer-sha-gap-detail-plan.md b/docs/plans/2026-05-24-117-defer-sha-gap-detail-plan.md new file mode 100644 index 000000000..67a02d1ee --- /dev/null +++ b/docs/plans/2026-05-24-117-defer-sha-gap-detail-plan.md @@ -0,0 +1,36 @@ +--- +title: "fix: defer briefing sha gap detail and preflight retry" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Defer SHA Gap Detail (plan 117) + +## Summary + +Defer `monitor_commands.preflight_retry` incorrectly copies the watch `command`. Fix to always single-shot preflight. Add structured **`sha_gap`** on defer when FC SHA lags master. + +--- + +## Problem Frame + +Live: `fc_active_pending`; `preflight_retry` duplicates preflight-watch; agents lack structured SHA fields beyond text notes. + +--- + +## Requirements + +- R1. `_build_defer_monitor_commands` always sets `preflight_retry` to `--lfg-preflight --json`. +- R2. `_build_defer_sha_gap_detail` exposes fc_head, master_sha, queued_hours. +- R3. Defer briefing attaches `sha_gap` when FC stale gap pending. +- R4. Defer stderr includes `sha_gap=` short form; tests; `PLAN_TRACK_CAP` `117`; docs. + +--- + +## Test scenarios + +- T1. Watch defer → preflight_retry is single-shot, command is watch. +- T2. fc_active_pending briefing includes sha_gap with head SHAs. +- T3. stderr includes sha_gap= prefix. diff --git a/docs/plans/2026-05-24-118-lfg-gate-watch-plan.md b/docs/plans/2026-05-24-118-lfg-gate-watch-plan.md new file mode 100644 index 000000000..2b147b3e9 --- /dev/null +++ b/docs/plans/2026-05-24-118-lfg-gate-watch-plan.md @@ -0,0 +1,37 @@ +--- +title: "feat: lfg gate-watch and defer post-terminal commands" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: LFG Gate-Watch + Post-Terminal Commands (plan 118) + +## Summary + +Agents repeatedly run `--lfg-gate` (exit 2) while FC is queued. Add **`--lfg-gate-watch`** (gate + preflight-watch) and defer **`post_terminal_commands`** for after FC completes. + +--- + +## Problem Frame + +Live: `fc_active_pending` with watch_recommended; gate exits 2 without polling; no structured next steps after FC terminal. + +--- + +## Requirements + +- R1. `--lfg-gate-watch` enables `--lfg-gate --lfg-preflight-watch`. +- R2. `_build_defer_post_terminal_commands` with preflight/prefetch-gate hints. +- R3. Defer briefing attaches `post_terminal_commands`; monitor_commands includes `gate_watch`. +- R4. `preflight_watch_summary.next_hint` from proceed_hint after watch. +- R5. `lfg_mode` `gate_watch`; tests; `PLAN_TRACK_CAP` `118`; docs. + +--- + +## Test scenarios + +- T1. `--lfg-gate-watch` sets gate + preflight-watch flags. +- T2. Defer briefing includes post_terminal_commands.prefetch_gate when fc_sha_stale. +- T3. Watch summary includes next_hint when defer clears. diff --git a/docs/plans/2026-05-24-119-defer-gate-watch-primary-plan.md b/docs/plans/2026-05-24-119-defer-gate-watch-primary-plan.md new file mode 100644 index 000000000..ef17bca61 --- /dev/null +++ b/docs/plans/2026-05-24-119-defer-gate-watch-primary-plan.md @@ -0,0 +1,36 @@ +--- +title: "fix: defer and drift wait prefer gate-watch command" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Prefer Gate-Watch as Primary Wait Command (plan 119) + +## Summary + +Defer and drift-wait briefings still primary-route to `--lfg-preflight-watch`. Prefer **`--lfg-gate-watch`** so agents poll once and get strict gate exit semantics. + +--- + +## Problem Frame + +Live: `fc_active_pending`; `gate_watch` exists in monitor_commands but `command`/`proceed_hint` use preflight-watch. + +--- + +## Requirements + +- R1. Defer `command` and `_build_proceed_hint` use `gate_watch` when watch recommended. +- R2. Drift wait uses `refresh_commands.gate_watch` as primary command. +- R3. Preflight watch poll stderr includes `sha_gap=` when present. +- R4. Tests; `PLAN_TRACK_CAP` `119`; closeout + plan 020 docs. + +--- + +## Test scenarios + +- T1. fc_active_pending defer → command is gate-watch. +- T2. investigate_ci_drift + active FC → command is gate-watch. +- T3. Watch poll line includes sha_gap when checkpoint stale. diff --git a/docs/plans/2026-05-24-120-fc-active-queue-backlog-plan.md b/docs/plans/2026-05-24-120-fc-active-queue-backlog-plan.md new file mode 100644 index 000000000..5ecd46d2d --- /dev/null +++ b/docs/plans/2026-05-24-120-fc-active-queue-backlog-plan.md @@ -0,0 +1,36 @@ +--- +title: "fix: fc-active defer queue backlog context" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: FC-Active Defer Queue Backlog (plan 120) + +## Summary + +`fc_active_pending` defer returns before `queue_backlog_note` is set. Surface queue backlog on that path and attach **`queue_context`** + **`primary_action`** to defer briefing. + +--- + +## Problem Frame + +Live: FC queued 0.2h (not severe yet); fc_active_pending path skips backlog note logic used by other defer branches. + +--- + +## Requirements + +- R1. fc_active_pending checkpoint sets `queue_backlog_note` when FC queued ≥ 4h. +- R2. `_build_defer_queue_context` exposes backlog flags and max queued hours. +- R3. Defer briefing includes `queue_context`, `primary_action: gate_watch`, and queue note in notes. +- R4. stderr `queue_backlog=true` when severe; tests; `PLAN_TRACK_CAP` `120`; docs. + +--- + +## Test scenarios + +- T1. FC active stale gap + queued 4.5h → checkpoint has queue_backlog_note. +- T2. Defer briefing queue_context.queue_backlog true when severe. +- T3. stderr includes queue_backlog=true. diff --git a/docs/plans/2026-05-24-121-gate-watch-poll-labels-plan.md b/docs/plans/2026-05-24-121-gate-watch-poll-labels-plan.md new file mode 100644 index 000000000..70950d80a --- /dev/null +++ b/docs/plans/2026-05-24-121-gate-watch-poll-labels-plan.md @@ -0,0 +1,54 @@ +--- +title: "fix: gate-watch poll labels and defer queued stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Gate-Watch Poll Labels and Defer Queued Stderr (plan 121) + +## Summary + +When agents run **`--lfg-gate-watch`**, poll stderr still says "preflight watch". Add mode-aware watch labels, surface **`max_queued_hours`** on defer briefing stderr, and include **`next_hint`** in watch summary lines. + +--- + +## Problem Frame + +Live defer: FC queued ~0.3h on `573c9d4` vs master `8916e2f`; primary wait is **`--lfg-gate-watch`**. Poll lines and summary stderr use preflight naming, and defer briefing stderr omits queued hours unless backlog is severe (≥4h). + +--- + +## Requirements + +- R1. `_format_preflight_watch_poll_line` accepts a watch label; gate mode prints `LFG gate watch poll`. +- R2. `_watch_lfg_preflight_defer` accepts `watch_label`; summary/next stderr use gate vs preflight naming. +- R3. `_emit_lfg_agent_briefing_stderr` adds `queued=0.3h` from `queue_context.max_queued_hours` on defer. +- R4. `_format_preflight_watch_summary_line` appends truncated `next_hint` when present. +- R5. Tests; `PLAN_TRACK_CAP` `121`; closeout doc bullet for plan 121. + +--- + +## Scope Boundaries + +- No change to exit codes, defer logic, or FC terminal classification. +- No browser/UI work. + +--- + +## Implementation Units + +- U1. **Watch label plumbing** — `watch_label` param on poll formatter and watch loop; main passes `gate` when `lfg_gate_watch`. +- U2. **Defer stderr queued hours** — emit `queued=X.Xh` from queue_context when defer action. +- U3. **Watch summary next_hint** — summary line suffix; gate vs preflight summary stderr prefix. +- U4. **Tests and docs** — unit tests; bump `PLAN_TRACK_CAP`; closeout Prefer bullet. + +--- + +## Test scenarios + +- T1. Poll line with `watch_label="gate"` contains `gate watch poll`. +- T2. Defer stderr with `queue_context.max_queued_hours=0.33` contains `queued=0.3h`. +- T3. Summary formatter with `next_hint` includes truncated hint in line. +- T4. Plan patch test expects `019–121`. diff --git a/docs/plans/2026-05-24-122-defer-expected-after-terminal-plan.md b/docs/plans/2026-05-24-122-defer-expected-after-terminal-plan.md new file mode 100644 index 000000000..61f8e213f --- /dev/null +++ b/docs/plans/2026-05-24-122-defer-expected-after-terminal-plan.md @@ -0,0 +1,45 @@ +--- +title: "fix: defer expected-after-terminal and queue warn" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Defer Expected-After-Terminal and Queue Warn (plan 122) + +## Summary + +During **`fc_active_pending`** defer, agents need explicit post-FC guidance and early queue warnings before the 4h severe threshold. Add **`expected_after_terminal`** to defer briefing, **`queue_backlog_warning`** at ≥2h queued, and defer-reason transition in watch summary stderr. + +--- + +## Problem Frame + +Live: FC queued 0.5h on stale SHA; agents must gate-watch then run prefetch+gate after terminal. Briefing has **`post_terminal_commands`** but no single primary **`expected_after_terminal`** field. Queue severity only surfaces at 4h. + +--- + +## Requirements + +- R1. Defer briefing includes **`expected_after_terminal`** `{action, command}` preferring `prefetch_gate` → `gate` → `preflight`. +- R2. **`queue_context.queue_backlog_warning`** when `max_queued_hours` ≥ 2 and < 4. +- R3. Defer stderr **`queue_warn=true`** when warning active. +- R4. Watch summary line includes **`reason=start->end`** when defer reasons differ across watch. +- R5. Tests; `PLAN_TRACK_CAP` `122`; closeout doc bullet. + +--- + +## Scope Boundaries + +- No change to defer exit codes or FC classification logic. +- No doc auto-apply while deferred. + +--- + +## Test scenarios + +- T1. Defer briefing with `prefetch_gate` post_terminal → `expected_after_terminal.action=prefetch_gate`. +- T2. queue_context warning at 2.5h queued, not severe. +- T3. stderr includes `queue_warn=true` and `expected_after=prefetch_gate`. +- T4. Summary line with differing start/end defer reasons includes `reason=`. diff --git a/docs/plans/2026-05-24-123-drift-expected-after-terminal-plan.md b/docs/plans/2026-05-24-123-drift-expected-after-terminal-plan.md new file mode 100644 index 000000000..7e244d4fe --- /dev/null +++ b/docs/plans/2026-05-24-123-drift-expected-after-terminal-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: drift expected-after-terminal and primary action" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Drift Expected-After-Terminal and Primary Action (plan 123) + +## Summary + +Live gate reports **`investigate_ci_drift`** with active FC/verify runs. Parity with defer briefing: add **`expected_after_terminal`**, **`primary_action: gate_watch`**, and **`queue_context`** on drift-wait paths; surface on stderr. + +--- + +## Requirements + +- R1. `_build_drift_expected_after(refresh_commands)` prefers closeout → refresh_dry_run → gate → preflight. +- R2. Drift briefing with `wait_recommended` sets `primary_action: gate_watch`, `expected_after_terminal`, and `queue_context`. +- R3. Drift briefing without wait sets `expected_after_terminal` to closeout when available. +- R4. Stderr for `investigate_ci_drift` includes `primary_action`, `expected_after`, and `queued=` when queue_context present. +- R5. Fix `expected_after` variable shadowing `action` in stderr emitter; tests; `PLAN_TRACK_CAP` 123; docs. + +--- + +## Test scenarios + +- T1. Active FC drift → `wait_recommended`, `primary_action=gate_watch`, `expected_after.action=refresh_dry_run`. +- T2. Terminal drift → `expected_after.action=closeout`. +- T3. stderr drift includes `expected_after=refresh_dry_run` and `primary_action=gate_watch`. +- T4. Plan patch expects `019–123`. diff --git a/docs/plans/2026-05-24-124-defer-active-runs-closeout-plan.md b/docs/plans/2026-05-24-124-defer-active-runs-closeout-plan.md new file mode 100644 index 000000000..523a9f47e --- /dev/null +++ b/docs/plans/2026-05-24-124-defer-active-runs-closeout-plan.md @@ -0,0 +1,33 @@ +--- +title: "fix: defer active_runs and closeout expected-after" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Defer Active Runs and Closeout Expected-After (plan 124) + +## Summary + +Live defer **`unchanged_active_runs`** with FC queued: agents need to know which runs block LFG and that **`--lfg-closeout`** follows terminal. Add **`active_runs`** to defer briefing/stderr/watch polls and prefer **`closeout`** in **`expected_after_terminal`** for closeout-style defer reasons. + +--- + +## Requirements + +- R1. `_build_active_runs_list(status)` shared helper; used by drift detail and defer briefing. +- R2. Defer briefing includes `active_runs`; stderr `active_runs=fc` (comma-separated). +- R3. Watch poll line includes `active_runs=` when any run active. +- R4. `_build_defer_post_terminal_commands` adds `closeout` for unchanged_active_runs / fc_active_closeout / verify_active_closeout. +- R5. `_build_defer_expected_after_terminal` order: prefetch_gate → closeout → gate → preflight. +- R6. Tests; `PLAN_TRACK_CAP` 124; closeout doc bullet. + +--- + +## Test scenarios + +- T1. FC-only active defer → briefing `active_runs=["fc"]`, stderr `active_runs=fc`. +- T2. unchanged_active_runs → `expected_after_terminal.action=closeout`. +- T3. Watch poll line includes `active_runs=fc`. +- T4. Plan patch expects `019–124`. diff --git a/docs/plans/2026-05-24-125-strict-exit-briefing-stderr-plan.md b/docs/plans/2026-05-24-125-strict-exit-briefing-stderr-plan.md new file mode 100644 index 000000000..60196d38c --- /dev/null +++ b/docs/plans/2026-05-24-125-strict-exit-briefing-stderr-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: strict exit stderr and verify_run briefing" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Strict Exit Stderr and Verify Run Briefing (plan 125) + +## Summary + +Live defer shows **`active_runs=verify,fc`** but **`LFG exit:`** omits expected-after context and **`verify_run`**. Enrich strict exit stderr from briefing; add **`verify_run=`** to briefing stderr; drift wait gets top-level **`active_runs`**. + +--- + +## Requirements + +- R1. `_emit_lfg_strict_exit_stderr` appends `expected_after`, `active_runs`, `primary_action` from `lfg_agent_briefing`. +- R2. Briefing stderr includes `verify_run=` when `verify_run_id` present. +- R3. Drift wait briefing sets top-level `active_runs`; drift stderr includes `active_runs=`. +- R4. Tests; `PLAN_TRACK_CAP` 125; closeout doc bullet. + +--- + +## Test scenarios + +- T1. Strict exit with defer briefing → line includes `expected_after=closeout active_runs=fc`. +- T2. Briefing stderr with verify_run_id → `verify_run=123`. +- T3. Drift wait briefing includes top-level `active_runs`. +- T4. Plan patch expects `019–125`. diff --git a/docs/plans/2026-05-24-126-gh-watch-multi-run-plan.md b/docs/plans/2026-05-24-126-gh-watch-multi-run-plan.md new file mode 100644 index 000000000..74910c1c1 --- /dev/null +++ b/docs/plans/2026-05-24-126-gh-watch-multi-run-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: gh_watch multi-run and watch summary active_runs" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: GH Watch Multi-Run and Watch Summary Active Runs (plan 126) + +## Summary + +Live defer has **`active_runs=verify,fc`** but stderr **`watch=`** only references FC. Add compact **`gh_watch=verify:ID,fc:ID`** and include **`active_runs`** in **`preflight_watch_summary`** JSON. + +--- + +## Requirements + +- R1. `_format_gh_watch_summary(briefing)` builds `verify:ID,fc:ID` from monitor commands. +- R2. Briefing stderr emits `gh_watch=` when any gh run watches exist (alongside legacy `watch=`). +- R3. `preflight_watch_summary` includes `active_runs` from final watch status. +- R4. Tests; `PLAN_TRACK_CAP` 126; closeout doc bullet. + +--- + +## Test scenarios + +- T1. Both runs active → stderr contains `gh_watch=verify:1,fc:2`. +- T2. FC-only → `gh_watch=fc:2`. +- T3. Watch summary JSON includes `active_runs`. +- T4. Plan patch expects `019–126`. diff --git a/docs/plans/2026-05-24-127-strict-exit-gh-watch-plan.md b/docs/plans/2026-05-24-127-strict-exit-gh-watch-plan.md new file mode 100644 index 000000000..6faf72af0 --- /dev/null +++ b/docs/plans/2026-05-24-127-strict-exit-gh-watch-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: strict exit gh_watch and watch summary active_runs stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Strict Exit gh_watch and Watch Summary active_runs Stderr (plan 127) + +## Summary + +Plan 126 added **`gh_watch=verify:ID,fc:ID`** to briefing stderr and **`active_runs`** in watch summary JSON. Agents reading only **`LFG exit:`** or the one-line watch summary still miss multi-run watch IDs. Propagate **`gh_watch`** to strict exit stderr, structured briefing JSON, and the watch summary one-liner. + +--- + +## Requirements + +- R1. Defer/drift briefing JSON includes **`gh_watch_summary`** when monitor watch commands exist. +- R2. **`LFG exit:`** stderr appends **`gh_watch=`** from briefing (parity with briefing stderr). +- R3. **`_format_preflight_watch_summary_line`** appends **`active_runs=`** when summary dict carries **`active_runs`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 127; closeout doc bullet; plans index **019–127**. + +--- + +## Test scenarios + +- T1. Strict exit defer briefing → stderr contains **`gh_watch=verify:1,fc:2`**. +- T2. Defer briefing JSON includes **`gh_watch_summary`**. +- T3. Watch summary line includes **`active_runs=verify,fc`**. +- T4. Plan patch expects **`019–127`**. diff --git a/docs/plans/2026-05-24-128-watch-summary-gh-watch-plan.md b/docs/plans/2026-05-24-128-watch-summary-gh-watch-plan.md new file mode 100644 index 000000000..f20e8c12e --- /dev/null +++ b/docs/plans/2026-05-24-128-watch-summary-gh-watch-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: preflight watch summary gh_watch json and stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Preflight Watch Summary gh_watch JSON and Stderr (plan 128) + +## Summary + +Plans 126–127 surfaced **`gh_watch`** on briefing and strict-exit stderr and **`active_runs`** on watch summary JSON/one-liner. Agents parsing **`preflight_watch_summary`** JSON still lack **`gh_watch_summary`**, and the watch summary stderr line omits **`gh_watch=`**. + +--- + +## Requirements + +- R1. **`_build_gh_watch_from_status(status)`** returns compact `verify:ID,fc:ID` for active runs. +- R2. **`preflight_watch_summary`** JSON includes **`gh_watch_summary`** when active watches exist. +- R3. **`_format_preflight_watch_summary_line`** appends **`gh_watch=`** when summary carries **`gh_watch_summary`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 128; closeout doc bullet; plans index **019–128**. + +--- + +## Test scenarios + +- T1. `_build_gh_watch_from_status` → `verify:1,fc:2` when both active. +- T2. Watch timeout summary JSON includes **`gh_watch_summary`**. +- T3. Watch summary stderr line includes **`gh_watch=verify:1,fc:2`**. +- T4. Plan patch expects **`019–128`**. diff --git a/docs/plans/2026-05-24-129-top-level-gh-watch-plan.md b/docs/plans/2026-05-24-129-top-level-gh-watch-plan.md new file mode 100644 index 000000000..2f8d12333 --- /dev/null +++ b/docs/plans/2026-05-24-129-top-level-gh-watch-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level gh_watch json and watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level gh_watch JSON and Watch Poll Stderr (plan 129) + +## Summary + +`gh_watch_summary` lives only under **`lfg_agent_briefing`** in gate JSON; agents scanning top-level keys miss multi-run watch IDs. Watch poll stderr lists per-run IDs and **`active_runs=`** but not compact **`gh_watch=`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`gh_watch_summary`** to top-level status JSON. +- R2. **`_format_preflight_watch_poll_line`** appends **`gh_watch=`** via **`_build_gh_watch_from_status`**. +- R3. Tests; **`PLAN_TRACK_CAP`** 129; closeout doc bullet; plans index **019–129**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`gh_watch_summary`** when deferred with active runs. +- T2. Watch poll line includes **`gh_watch=verify:1,fc:2`**. +- T3. Plan patch expects **`019–129`**. diff --git a/docs/plans/2026-05-24-130-top-level-active-runs-plan.md b/docs/plans/2026-05-24-130-top-level-active-runs-plan.md new file mode 100644 index 000000000..60d0437e2 --- /dev/null +++ b/docs/plans/2026-05-24-130-top-level-active-runs-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level active_runs json and strict exit queued stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level active_runs JSON and Strict Exit queued Stderr (plan 130) + +## Summary + +Plan 129 mirrored **`gh_watch_summary`** to top-level gate JSON. **`active_runs`** still requires drilling into **`lfg_agent_briefing`**. Briefing stderr emits **`queued=X.Xh`** but **`LFG exit:`** does not. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`active_runs`** to top-level status JSON. +- R2. **`_emit_lfg_strict_exit_stderr`** appends **`queued=X.Xh`** from briefing **`queue_context.max_queued_hours`**. +- R3. Tests; **`PLAN_TRACK_CAP`** 130; closeout doc bullet; plans index **019–130**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`active_runs`** when deferred with active runs. +- T2. Strict exit defer briefing → stderr contains **`queued=1.5h`**. +- T3. Plan patch expects **`019–130`**. diff --git a/docs/plans/2026-05-24-131-queue-context-watch-summary-plan.md b/docs/plans/2026-05-24-131-queue-context-watch-summary-plan.md new file mode 100644 index 000000000..56783bcb6 --- /dev/null +++ b/docs/plans/2026-05-24-131-queue-context-watch-summary-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: top-level queue_context and watch summary queued stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level queue_context and Watch Summary queued Stderr (plan 131) + +## Summary + +Plans 129–130 surfaced **`gh_watch_summary`** and **`active_runs`** at top-level gate JSON and **`queued=`** on strict exit. **`queue_context`** still requires drilling into **`lfg_agent_briefing`**, and watch summary stderr lacks aggregate **`queued=`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`queue_context`** to top-level status JSON. +- R2. **`preflight_watch_summary`** JSON includes **`queue_context`** from **`_build_defer_queue_context`**. +- R3. **`_format_preflight_watch_summary_line`** appends **`queued=`** and queue flags from summary **`queue_context`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 131; closeout doc bullet; plans index **019–131**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`queue_context.max_queued_hours`** when deferred. +- T2. Watch summary JSON includes **`queue_context`**; one-liner has **`queued=1.5h`**. +- T3. Plan patch expects **`019–131`**. diff --git a/docs/plans/2026-05-24-132-expected-after-top-level-plan.md b/docs/plans/2026-05-24-132-expected-after-top-level-plan.md new file mode 100644 index 000000000..1edc51120 --- /dev/null +++ b/docs/plans/2026-05-24-132-expected-after-top-level-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: top-level expected_after and watch summary primary_action" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level expected_after and Watch Summary primary_action (plan 132) + +## Summary + +Strict exit stderr carries **`expected_after=closeout`** and **`primary_action=gate_watch`**, but top-level gate JSON and **`preflight_watch_summary`** omit them unless agents drill into **`lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`expected_after_terminal`** and **`primary_action`** to top-level status JSON. +- R2. **`preflight_watch_summary`** JSON includes both when defer briefing applies on watch end. +- R3. **`_format_preflight_watch_summary_line`** appends **`expected_after=`** and **`primary_action=`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 132; closeout doc bullet; plans index **019–132**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`expected_after_terminal.action=closeout`** and **`primary_action=gate_watch`** when deferred. +- T2. Watch summary JSON/one-liner include **`expected_after=closeout`** and **`primary_action=gate_watch`**. +- T3. Plan patch expects **`019–132`**. diff --git a/docs/plans/2026-05-24-133-watch-poll-queued-plan.md b/docs/plans/2026-05-24-133-watch-poll-queued-plan.md new file mode 100644 index 000000000..bd8030c20 --- /dev/null +++ b/docs/plans/2026-05-24-133-watch-poll-queued-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: watch poll queued and expected_after stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Watch Poll queued and expected_after Stderr (plan 133) + +## Summary + +Watch poll stderr lists per-run **`fc_queued=`** / **`verify_queued=`** and **`gh_watch=`**, but lacks aggregate **`queued=`**, queue flags, **`expected_after=`**, and **`primary_action=`** present on **`LFG exit:`** and watch summary lines. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends aggregate **`queued=`** and queue flags from **`_build_defer_queue_context`**. +- R2. Poll line appends **`expected_after=`** and **`primary_action=`** from defer briefing when **`lfg_deferred`**. +- R3. Tests; **`PLAN_TRACK_CAP`** 133; closeout doc bullet; plans index **019–133**. + +--- + +## Test scenarios + +- T1. Poll line includes **`queued=1.5h`** and **`queue_warn=true`** when warning threshold met. +- T2. Poll line includes **`expected_after=closeout`** and **`primary_action=gate_watch`** when deferred. +- T3. Plan patch expects **`019–133`**. diff --git a/docs/plans/2026-05-24-134-watch-recommended-top-level-plan.md b/docs/plans/2026-05-24-134-watch-recommended-top-level-plan.md new file mode 100644 index 000000000..5aef084c6 --- /dev/null +++ b/docs/plans/2026-05-24-134-watch-recommended-top-level-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: top-level watch_recommended json and stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level watch_recommended JSON and Stderr (plan 134) + +## Summary + +Defer briefing sets **`watch_recommended: true`** and briefing stderr emits **`watch_recommended=true`**, but top-level gate JSON and watch summary omit it unless agents drill into **`lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`watch_recommended`** to top-level status JSON when true. +- R2. **`LFG exit:`** stderr appends **`watch_recommended=true`** when briefing recommends watch. +- R3. **`preflight_watch_summary`** JSON and one-liner include **`watch_recommended`** when set. +- R4. Tests; **`PLAN_TRACK_CAP`** 134; closeout doc bullet; plans index **019–134**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level **`watch_recommended: true`** when deferred with active runs. +- T2. Strict exit stderr includes **`watch_recommended=true`**. +- T3. Watch summary one-liner includes **`watch_recommended=true`**. +- T4. Plan patch expects **`019–134`**. diff --git a/docs/plans/2026-05-24-135-post-terminal-top-level-plan.md b/docs/plans/2026-05-24-135-post-terminal-top-level-plan.md new file mode 100644 index 000000000..8cd3f5c0c --- /dev/null +++ b/docs/plans/2026-05-24-135-post-terminal-top-level-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: top-level post_terminal_commands and poll watch_recommended" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level post_terminal_commands and Poll watch_recommended (plan 135) + +## Summary + +Defer briefing includes **`post_terminal_commands`** (preflight/gate/closeout) for after verify+FC terminal, but gate JSON and watch summary omit it unless agents drill into **`lfg_agent_briefing`**. Watch poll stderr also lacks **`watch_recommended=true`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`post_terminal_commands`** to top-level status JSON when present. +- R2. **`preflight_watch_summary`** JSON includes **`post_terminal_commands`** on deferred watch end. +- R3. Watch poll stderr appends **`watch_recommended=true`** when defer briefing recommends watch. +- R4. Tests; **`PLAN_TRACK_CAP`** 135; closeout doc bullet; plans index **019–135**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`post_terminal_commands.closeout`** when deferred. +- T2. Watch summary JSON includes **`post_terminal_commands`**. +- T3. Poll line includes **`watch_recommended=true`** when deferred with watch recommended. +- T4. Plan patch expects **`019–135`**. diff --git a/docs/plans/2026-05-24-136-wait-command-top-level-plan.md b/docs/plans/2026-05-24-136-wait-command-top-level-plan.md new file mode 100644 index 000000000..adcc57f18 --- /dev/null +++ b/docs/plans/2026-05-24-136-wait-command-top-level-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level wait_command and monitor_commands json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level wait_command and monitor_commands JSON (plan 136) + +## Summary + +Defer briefing carries **`command`** (gate-watch wait) and structured **`monitor_commands`**, but gate JSON requires drilling into **`lfg_agent_briefing`**. Live queue age now triggers **`queue_warn=true`** at ≥2h — agents need the wait command at top level. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`wait_command`** from briefing **`command`** and **`monitor_commands`** when present. +- R2. **`preflight_watch_summary`** JSON includes **`wait_command`** and **`monitor_commands`** on deferred watch end. +- R3. Tests; **`PLAN_TRACK_CAP`** 136; closeout doc bullet; plans index **019–136**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`wait_command`** and **`monitor_commands.gate_watch`** when deferred. +- T2. Watch summary JSON includes **`wait_command`** and **`monitor_commands`**. +- T3. Plan patch expects **`019–136`**. diff --git a/docs/plans/2026-05-24-137-run-id-top-level-plan.md b/docs/plans/2026-05-24-137-run-id-top-level-plan.md new file mode 100644 index 000000000..9806cd442 --- /dev/null +++ b/docs/plans/2026-05-24-137-run-id-top-level-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level verify and fc run id json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level verify_run_id and fc_run_id JSON (plan 137) + +## Summary + +Defer briefing exposes **`verify_run_id`** / **`fc_run_id`**, and stderr carries **`verify_run=`** / **`fc_run=`**, but top-level gate JSON omits run IDs unless agents drill into **`lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`verify_run_id`** and **`fc_run_id`** to top-level status JSON when set. +- R2. **`preflight_watch_summary`** JSON includes both run IDs on deferred watch end. +- R3. Tests; **`PLAN_TRACK_CAP`** 137; closeout doc bullet; plans index **019–137**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`verify_run_id`** and **`fc_run_id`** when deferred with active runs. +- T2. Watch summary JSON includes both run IDs. +- T3. Plan patch expects **`019–137`**. diff --git a/docs/plans/2026-05-24-138-run-url-top-level-plan.md b/docs/plans/2026-05-24-138-run-url-top-level-plan.md new file mode 100644 index 000000000..ea5e74ccb --- /dev/null +++ b/docs/plans/2026-05-24-138-run-url-top-level-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: top-level verify and fc run url json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level verify_run_url and fc_run_url JSON (plan 138) + +## Summary + +Defer briefing exposes **`verify_run_url`** / **`fc_run_url`** via **`_attach_active_run_refs`**, but top-level gate JSON and **`preflight_watch_summary`** omit URLs unless agents drill into **`lfg_agent_briefing`**. Strict exit stderr also omits **`verify_run=`** / **`fc_run=`** IDs present on the briefing line. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`verify_run_url`** and **`fc_run_url`** to top-level status JSON when set. +- R2. **`preflight_watch_summary`** JSON includes both run URLs on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`verify_run=`** / **`fc_run=`** when briefing carries run IDs. +- R4. Tests; **`PLAN_TRACK_CAP`** 138; closeout doc bullet; plans index **019–138**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`verify_run_url`** and **`fc_run_url`** when deferred with active runs. +- T2. Watch summary JSON includes both run URLs. +- T3. Strict exit stderr includes **`verify_run=`** and **`fc_run=`** when deferred. +- T4. Plan patch expects **`019–138`**. diff --git a/docs/plans/2026-05-24-139-run-status-top-level-plan.md b/docs/plans/2026-05-24-139-run-status-top-level-plan.md new file mode 100644 index 000000000..017b7367c --- /dev/null +++ b/docs/plans/2026-05-24-139-run-status-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level verify and fc status json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level verify_status and fc_status JSON (plan 139) + +## Summary + +Defer briefing exposes **`verify_status`** / **`fc_status`** via **`_attach_active_run_refs`**, and watch poll stderr already prints **`verify_status=`** / **`fc_status=`**, but top-level gate JSON and **`preflight_watch_summary`** omit status words unless agents drill into **`lfg_agent_briefing`**. Strict exit stderr also omits them. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`verify_status`** and **`fc_status`** to top-level status JSON when set. +- R2. **`preflight_watch_summary`** JSON includes both status words on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`verify_status=`** / **`fc_status=`** when briefing carries them. +- R4. Watch summary one-liner includes **`verify_status=`** / **`fc_status=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 139; closeout doc bullet; plans index **019–139**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`verify_status`** and **`fc_status`** when deferred with active runs. +- T2. Watch summary JSON includes both status words. +- T3. Strict exit stderr includes **`verify_status=`** and **`fc_status=`**. +- T4. Plan patch expects **`019–139`**. diff --git a/docs/plans/2026-05-24-140-blocked-top-level-plan.md b/docs/plans/2026-05-24-140-blocked-top-level-plan.md new file mode 100644 index 000000000..7f85ca9a3 --- /dev/null +++ b/docs/plans/2026-05-24-140-blocked-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level lfg briefing blocked json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level blocked JSON (plan 140) + +## Summary + +Defer briefing exposes **`blocked: deferred`**, and the briefing stderr line prints **`blocked=deferred`**, but top-level gate JSON and **`preflight_watch_summary`** omit **`blocked`** unless agents drill into **`lfg_agent_briefing`**. Strict exit stderr also omits it. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`blocked`** to top-level status JSON when set. +- R2. **`preflight_watch_summary`** JSON includes **`blocked`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`blocked=`** when briefing carries it. +- R4. Watch summary one-liner includes **`blocked=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 140; closeout doc bullet; plans index **019–140**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`blocked: deferred`** when deferred. +- T2. Watch summary JSON includes **`blocked`**. +- T3. Strict exit stderr includes **`blocked=deferred`**. +- T4. Plan patch expects **`019–140`**. diff --git a/docs/plans/2026-05-24-141-queue-flags-top-level-plan.md b/docs/plans/2026-05-24-141-queue-flags-top-level-plan.md new file mode 100644 index 000000000..3751b0bb4 --- /dev/null +++ b/docs/plans/2026-05-24-141-queue-flags-top-level-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level queue backlog flags json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level queue backlog flags JSON (plan 141) + +## Summary + +Defer **`queue_context`** nests **`queue_backlog`**, **`queue_backlog_severe`**, **`queue_backlog_warning`**, and **`max_queued_hours`**, but agents scanning top-level gate JSON must drill into the object even though stderr already prints **`queue_warn=`** / **`queue_backlog=`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors queue backlog flags and **`max_queued_hours`** to top-level status JSON from **`queue_context`**. +- R2. **`preflight_watch_summary`** JSON includes the same flattened queue fields on deferred watch end. +- R3. Tests; **`PLAN_TRACK_CAP`** 141; closeout doc bullet; plans index **019–141**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`queue_backlog_warning: true`** and **`max_queued_hours`** when deferred with ≥2h queue. +- T2. Watch summary JSON includes flattened queue fields. +- T3. Plan patch expects **`019–141`**. diff --git a/docs/plans/2026-05-24-142-briefing-action-top-level-plan.md b/docs/plans/2026-05-24-142-briefing-action-top-level-plan.md new file mode 100644 index 000000000..74d355920 --- /dev/null +++ b/docs/plans/2026-05-24-142-briefing-action-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level briefing action json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_action JSON (plan 142) + +## Summary + +Defer briefing exposes **`action: defer`**, and the briefing stderr line prints **`action=defer`**, but top-level gate JSON omits the action word unless agents drill into **`lfg_agent_briefing`**. Strict exit stderr also omits **`action=`** even though **`lfg_defer_reason`** is present separately. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`action`** to top-level **`briefing_action`** when set. +- R2. **`preflight_watch_summary`** JSON includes **`briefing_action`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`action=`** when briefing carries it. +- R4. Watch summary one-liner includes **`action=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 142; closeout doc bullet; plans index **019–142**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`briefing_action: defer`** when deferred. +- T2. Watch summary JSON includes **`briefing_action`**. +- T3. Strict exit stderr includes **`action=defer`**. +- T4. Plan patch expects **`019–142`**. diff --git a/docs/plans/2026-05-24-143-briefing-notes-top-level-plan.md b/docs/plans/2026-05-24-143-briefing-notes-top-level-plan.md new file mode 100644 index 000000000..d3ed9b5af --- /dev/null +++ b/docs/plans/2026-05-24-143-briefing-notes-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level briefing notes json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_notes JSON (plan 143) + +## Summary + +Defer briefing may include checkpoint **`notes`** (e.g. **`queue_backlog_note`**, **`fc_stale_gap_pending_note`**), but top-level gate JSON omits them unless agents drill into **`lfg_agent_briefing`**. Strict exit and watch summary stderr also omit a compact notes signal. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors non-empty **`notes`** to top-level **`briefing_notes`**. +- R2. **`preflight_watch_summary`** JSON includes **`briefing_notes`** on deferred watch end when present. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`notes=N`** when briefing carries notes. +- R4. Watch summary one-liner includes **`notes=N`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 143; closeout doc bullet; plans index **019–143**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`briefing_notes`** when checkpoint notes populate briefing. +- T2. Watch summary JSON includes **`briefing_notes`** when present. +- T3. Strict exit stderr includes **`notes=1`** when briefing has one note. +- T4. Plan patch expects **`019–143`**. diff --git a/docs/plans/2026-05-24-144-briefing-reason-top-level-plan.md b/docs/plans/2026-05-24-144-briefing-reason-top-level-plan.md new file mode 100644 index 000000000..170d9beac --- /dev/null +++ b/docs/plans/2026-05-24-144-briefing-reason-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level briefing reason json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_reason JSON (plan 144) + +## Summary + +Defer briefing exposes **`reason: unchanged_active_runs`**, and the briefing stderr line prints **`reason=unchanged_active_runs`**, but top-level gate JSON only has the separate **`lfg_defer_reason`** key. Agents parsing briefing-shaped JSON without that mapping must drill into **`lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors briefing **`reason`** to top-level **`briefing_reason`** when set. +- R2. **`preflight_watch_summary`** JSON includes **`briefing_reason`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`briefing_reason=`** when briefing carries reason. +- R4. Watch summary one-liner includes **`briefing_reason=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 144; closeout doc bullet; plans index **019–144**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`briefing_reason: unchanged_active_runs`** when deferred. +- T2. Watch summary JSON includes **`briefing_reason`**. +- T3. Strict exit stderr includes **`briefing_reason=unchanged_active_runs`**. +- T4. Plan patch expects **`019–144`**. diff --git a/docs/plans/2026-05-24-145-briefing-merge-ready-plan.md b/docs/plans/2026-05-24-145-briefing-merge-ready-plan.md new file mode 100644 index 000000000..8ffc129d0 --- /dev/null +++ b/docs/plans/2026-05-24-145-briefing-merge-ready-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level briefing merge ready json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_merge_ready JSON (plan 145) + +## Summary + +Defer briefing always sets **`merge_ready: false`**, but top-level gate JSON omits it unless agents drill into **`lfg_agent_briefing`**. This completes defer-briefing field mirroring alongside **`briefing_action`**, **`briefing_reason`**, and **`briefing_notes`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`merge_ready`** to top-level **`briefing_merge_ready`** when present in briefing. +- R2. **`preflight_watch_summary`** JSON includes **`briefing_merge_ready`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`merge_ready=false`** when briefing sets **`merge_ready`** false. +- R4. Watch summary one-liner includes **`merge_ready=false`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 145; closeout doc bullet; plans index **019–145**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`briefing_merge_ready: false`** when deferred. +- T2. Watch summary JSON includes **`briefing_merge_ready`**. +- T3. Strict exit stderr includes **`merge_ready=false`**. +- T4. Plan patch expects **`019–145`**. diff --git a/docs/plans/2026-05-24-146-queue-backlog-note-top-level-plan.md b/docs/plans/2026-05-24-146-queue-backlog-note-top-level-plan.md new file mode 100644 index 000000000..24190cf47 --- /dev/null +++ b/docs/plans/2026-05-24-146-queue-backlog-note-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level queue backlog note json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level queue_backlog_note JSON (plan 146) + +## Summary + +Defer **`queue_context`** nests a human-readable **`note`** copied from checkpoint **`queue_backlog_note`**, but top-level gate JSON requires drilling into **`queue_context`** even though **`briefing_notes`** may duplicate it as an array entry. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`queue_context.note`** to top-level **`queue_backlog_note`** when set. +- R2. **`preflight_watch_summary`** JSON includes **`queue_backlog_note`** on deferred watch end when present. +- R3. **`_emit_lfg_strict_exit_stderr`** appends truncated **`queue_note=`** when note is present. +- R4. Watch summary one-liner includes truncated **`queue_note=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 146; closeout doc bullet; plans index **019–146**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`queue_backlog_note`** when queue context carries a note. +- T2. Watch summary JSON includes **`queue_backlog_note`**. +- T3. Strict exit stderr includes **`queue_note=`** prefix when note present. +- T4. Plan patch expects **`019–146`**. diff --git a/docs/plans/2026-05-24-147-sha-gap-top-level-plan.md b/docs/plans/2026-05-24-147-sha-gap-top-level-plan.md new file mode 100644 index 000000000..505e38579 --- /dev/null +++ b/docs/plans/2026-05-24-147-sha-gap-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level sha gap json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level sha_gap JSON (plan 147) + +## Summary + +When FC SHA lag is active, defer briefing includes structured **`sha_gap`**, and briefing stderr prints **`sha_gap=`**, but top-level gate JSON omits the object unless agents drill into **`lfg_agent_briefing`**. Strict exit stderr also omits **`sha_gap=`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`sha_gap`** and **`sha_gap_short`** to top-level status JSON when set. +- R2. **`preflight_watch_summary`** JSON includes both when deferred with SHA gap. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`sha_gap=`** when briefing carries **`sha_gap.short`**. +- R4. Watch summary one-liner includes **`sha_gap=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 147; closeout doc bullet; plans index **019–147**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`sha_gap`** / **`sha_gap_short`** when FC SHA gap is active. +- T2. Watch summary JSON includes both fields. +- T3. Strict exit stderr includes **`sha_gap=7d85438:8916e2f`** style token. +- T4. Plan patch expects **`019–147`**. diff --git a/docs/plans/2026-05-24-148-gh-watch-command-top-level-plan.md b/docs/plans/2026-05-24-148-gh-watch-command-top-level-plan.md new file mode 100644 index 000000000..078804db4 --- /dev/null +++ b/docs/plans/2026-05-24-148-gh-watch-command-top-level-plan.md @@ -0,0 +1,32 @@ +--- +title: "fix: top-level gh watch command json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level gh_watch_command JSON (plan 148) + +## Summary + +Defer briefing stderr prints **`watch=gh run watch …`**, but top-level gate JSON only exposes the long **`wait_command`** gate-watch script unless agents drill into **`monitor_commands`**. Strict exit stderr also omits **`watch=`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors primary **`gh run watch`** command to top-level **`gh_watch_command`** when set. +- R2. **`preflight_watch_summary`** JSON includes **`gh_watch_command`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends **`watch=`** when briefing carries a gh watch command. +- R4. Watch summary one-liner includes **`watch=`** when present. +- R5. Tests; **`PLAN_TRACK_CAP`** 148; closeout doc bullet; plans index **019–148**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`gh_watch_command`** when deferred with active FC run. +- T2. Watch summary JSON includes **`gh_watch_command`**. +- T3. Strict exit stderr includes **`watch=gh run watch`**. +- T4. Plan patch expects **`019–148`**. diff --git a/docs/plans/2026-05-24-149-briefing-command-top-level-plan.md b/docs/plans/2026-05-24-149-briefing-command-top-level-plan.md new file mode 100644 index 000000000..73a201ac0 --- /dev/null +++ b/docs/plans/2026-05-24-149-briefing-command-top-level-plan.md @@ -0,0 +1,34 @@ +--- +title: "fix: top-level briefing_command json" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_command JSON (plan 149) + +## Summary + +Gate JSON exposes **`wait_command`** from defer briefing, but agents scanning **`briefing_*`** top-level fields miss the primary gate-watch script. Strict exit already uses **`command=`** for **`pr_ci_recommendation`**, so briefing needs a distinct **`briefing_command`** alias. Watch poll stderr also omits **`watch=`** and **`briefing_command=`** on deferred polls. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`briefing.command`** to top-level **`briefing_command`** (alongside **`wait_command`**). +- R2. **`preflight_watch_summary`** JSON includes **`briefing_command`** on deferred watch end. +- R3. **`_emit_lfg_strict_exit_stderr`** appends truncated **`briefing_command=`** when briefing carries a command (distinct from **`pr_ci_recommendation.command`**). +- R4. Watch summary one-liner includes truncated **`briefing_command=`** when present. +- R5. Watch poll stderr adds **`watch=`** and truncated **`briefing_command=`** when deferred briefing is applied. +- R6. Tests; **`PLAN_TRACK_CAP`** 149; closeout doc bullet; plans index **019–149**. + +--- + +## Test scenarios + +- T1. Gate JSON top-level includes **`briefing_command`** when deferred with gate-watch primary command. +- T2. Watch summary JSON includes **`briefing_command`**. +- T3. Strict exit stderr includes **`briefing_command=--lfg-gate-watch`** (truncated when long). +- T4. Watch poll stderr includes **`watch=`** and **`briefing_command=`** on deferred poll. +- T5. Plan patch expects **`019–149`**. diff --git a/docs/plans/2026-05-24-150-queue-note-watch-poll-plan.md b/docs/plans/2026-05-24-150-queue-note-watch-poll-plan.md new file mode 100644 index 000000000..678f3cf0b --- /dev/null +++ b/docs/plans/2026-05-24-150-queue-note-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: queue_note on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: queue_note on Defer Watch Poll stderr (plan 150) + +## Summary + +Plan 146 flattened **`queue_backlog_note`** to top-level gate JSON and added truncated **`queue_note=`** on strict exit and watch summary one-liners. Deferred **watch poll** stderr still omits **`queue_note=`**, so agents polling **`--lfg-gate-watch`** miss runner backlog context until the watch ends. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends truncated **`queue_note=`** when **`queue_backlog_note`** is present after **`_apply_lfg_agent_briefing`** (gate and preflight poll labels). +- R2. Tests for preflight and gate poll lines with deferred briefing carrying queue backlog note. +- R3. **`PLAN_TRACK_CAP`** 150; closeout doc bullet; plans index **019–150**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`queue_note=`** when deferred with checkpoint queue backlog note. +- T2. Gate watch poll stderr includes **`queue_note=`** on the same fixture. +- T3. Plan patch expects **`019–150`**. diff --git a/docs/plans/2026-05-24-151-blocked-watch-poll-plan.md b/docs/plans/2026-05-24-151-blocked-watch-poll-plan.md new file mode 100644 index 000000000..420e1d60c --- /dev/null +++ b/docs/plans/2026-05-24-151-blocked-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: blocked on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: blocked on Defer Watch Poll stderr (plan 151) + +## Summary + +Plan 140 flattened **`blocked`** to top-level gate JSON and added **`blocked=`** on strict exit and watch summary one-liners. Deferred watch poll stderr still omits **`blocked=`**, so agents polling **`--lfg-gate-watch`** cannot see defer vs other blocked states on each poll. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`blocked=`** when top-level **`blocked`** is set after **`_apply_lfg_agent_briefing`**. +- R2. Tests for preflight and gate poll lines with deferred **`blocked=deferred`**. +- R3. **`PLAN_TRACK_CAP`** 151; closeout doc bullet; plans index **019–151**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`blocked=deferred`** when deferred. +- T2. Gate watch poll stderr includes **`blocked=deferred`** on the same fixture. +- T3. Plan patch expects **`019–151`**. diff --git a/docs/plans/2026-05-24-152-briefing-reason-watch-poll-plan.md b/docs/plans/2026-05-24-152-briefing-reason-watch-poll-plan.md new file mode 100644 index 000000000..c72f101e0 --- /dev/null +++ b/docs/plans/2026-05-24-152-briefing-reason-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: briefing_reason on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: briefing_reason on Defer Watch Poll stderr (plan 152) + +## Summary + +Plan 144 flattened **`briefing_reason`** to top-level gate JSON and added **`briefing_reason=`** on strict exit and watch summary one-liners. Deferred watch poll stderr still omits **`briefing_reason=`**, so agents polling **`--lfg-gate-watch`** only see the raw **`lfg_defer_reason`** prefix and miss the normalized briefing reason on each poll. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`briefing_reason=`** when top-level **`briefing_reason`** is set after **`_apply_lfg_agent_briefing`**. +- R2. Tests for preflight and gate poll lines with deferred **`briefing_reason=unchanged_active_runs`**. +- R3. **`PLAN_TRACK_CAP`** 152; closeout doc bullet; plans index **019–152**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`briefing_reason=unchanged_active_runs`** when deferred. +- T2. Gate watch poll stderr includes **`briefing_reason=unchanged_active_runs`** on the same fixture. +- T3. Plan patch expects **`019–152`**. diff --git a/docs/plans/2026-05-24-153-briefing-action-watch-poll-plan.md b/docs/plans/2026-05-24-153-briefing-action-watch-poll-plan.md new file mode 100644 index 000000000..266b5d0d1 --- /dev/null +++ b/docs/plans/2026-05-24-153-briefing-action-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: briefing action on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: briefing_action on Defer Watch Poll stderr (plan 153) + +## Summary + +Plan 142 flattened **`briefing_action`** to top-level gate JSON and added **`action=`** on strict exit and watch summary one-liners. Deferred watch poll stderr still omits **`action=`**, so agents polling **`--lfg-gate-watch`** cannot see the briefing action (e.g. **`defer`**) on each poll. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`action=`** when top-level **`briefing_action`** is set after **`_apply_lfg_agent_briefing`**. +- R2. Tests for preflight and gate poll lines with deferred **`action=defer`**. +- R3. **`PLAN_TRACK_CAP`** 153; closeout doc bullet; plans index **019–153**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`action=defer`** when deferred. +- T2. Gate watch poll stderr includes **`action=defer`** on the same fixture. +- T3. Plan patch expects **`019–153`**. diff --git a/docs/plans/2026-05-24-154-briefing-notes-watch-poll-plan.md b/docs/plans/2026-05-24-154-briefing-notes-watch-poll-plan.md new file mode 100644 index 000000000..13cfeb4b2 --- /dev/null +++ b/docs/plans/2026-05-24-154-briefing-notes-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: briefing notes count on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: briefing_notes count on Defer Watch Poll stderr (plan 154) + +## Summary + +Plan 143 flattened **`briefing_notes`** to top-level gate JSON and added **`notes=N`** on strict exit and watch summary one-liners. Deferred watch poll stderr still omits **`notes=`**, so agents polling **`--lfg-gate-watch`** cannot see how many briefing notes apply on each poll. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`notes=N`** when **`briefing_notes`** is non-empty after **`_apply_lfg_agent_briefing`**. +- R2. Tests for preflight and gate poll lines with deferred briefing notes. +- R3. **`PLAN_TRACK_CAP`** 154; closeout doc bullet; plans index **019–154**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`notes=1`** when deferred with checkpoint queue backlog note. +- T2. Gate watch poll stderr includes **`notes=1`** on the same fixture. +- T3. Plan patch expects **`019–154`**. diff --git a/docs/plans/2026-05-24-155-briefing-merge-ready-watch-poll-plan.md b/docs/plans/2026-05-24-155-briefing-merge-ready-watch-poll-plan.md new file mode 100644 index 000000000..d50e36f02 --- /dev/null +++ b/docs/plans/2026-05-24-155-briefing-merge-ready-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: merge_ready on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: briefing_merge_ready on Defer Watch Poll stderr (plan 155) + +## Summary + +Plan 145 flattened **`briefing_merge_ready`** to top-level gate JSON and added **`merge_ready=`** on strict exit and watch summary one-liners. Deferred watch poll stderr still omits **`merge_ready=`**, so agents polling **`--lfg-gate-watch`** cannot see merge-readiness on each poll. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`merge_ready=`** when briefing carries **`merge_ready`** after **`_apply_lfg_agent_briefing`**. +- R2. Tests for preflight and gate poll lines with deferred **`merge_ready=false`**. +- R3. **`PLAN_TRACK_CAP`** 155; closeout doc bullet; plans index **019–155**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`merge_ready=false`** when deferred. +- T2. Gate watch poll stderr includes **`merge_ready=false`** on the same fixture. +- T3. Plan patch expects **`019–155`**. diff --git a/docs/plans/2026-05-24-156-run-id-watch-poll-plan.md b/docs/plans/2026-05-24-156-run-id-watch-poll-plan.md new file mode 100644 index 000000000..277e05580 --- /dev/null +++ b/docs/plans/2026-05-24-156-run-id-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: verify_run fc_run on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: verify_run / fc_run on Defer Watch Poll stderr (plan 156) + +## Summary + +Plan 137 flattened **`verify_run_id`** / **`fc_run_id`** to top-level gate JSON and plan 138 added strict-exit **`verify_run=`** / **`fc_run=`** tokens. Deferred watch poll stderr uses legacy **`verify=`** / **`fc=`** run keys only, so agents parsing strict-exit-style run IDs miss them unless they know both naming schemes. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** appends **`verify_run=`** / **`fc_run=`** from top-level mirrored IDs after **`_apply_lfg_agent_briefing`** when set. +- R2. Tests for preflight and gate poll lines with both run IDs present. +- R3. **`PLAN_TRACK_CAP`** 156; closeout doc bullet; plans index **019–156**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`verify_run=1`** and **`fc_run=2`** when deferred with active runs. +- T2. Gate watch poll stderr includes the same tokens. +- T3. Plan patch expects **`019–156`**. diff --git a/docs/plans/2026-05-24-157-run-status-watch-poll-plan.md b/docs/plans/2026-05-24-157-run-status-watch-poll-plan.md new file mode 100644 index 000000000..11d635487 --- /dev/null +++ b/docs/plans/2026-05-24-157-run-status-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: mirrored run status on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level verify_status / fc_status on Defer Watch Poll stderr (plan 157) + +## Summary + +Plan 139 flattened **`verify_status`** / **`fc_status`** to top-level gate JSON and added them on strict exit and watch summary one-liners. Deferred watch poll stderr still derives **`verify_status=`** / **`fc_status=`** only from raw run dicts before briefing apply, so poll tokens can drift from top-level mirror when briefing adjusts labels. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`verify_status=`** / **`fc_status=`** from top-level mirrored fields after **`_apply_lfg_agent_briefing`** when deferred. +- R2. Skip run-dict status tokens when **`lfg_deferred`** so poll stderr does not duplicate them. +- R3. Tests; **`PLAN_TRACK_CAP`** 157; closeout doc bullet; plans index **019–157**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`verify_status=queued`** and **`fc_status=queued`** once each when deferred. +- T2. Gate watch poll stderr includes the same tokens once each. +- T3. Plan patch expects **`019–157`**. diff --git a/docs/plans/2026-05-24-158-gh-watch-summary-watch-poll-plan.md b/docs/plans/2026-05-24-158-gh-watch-summary-watch-poll-plan.md new file mode 100644 index 000000000..11554777c --- /dev/null +++ b/docs/plans/2026-05-24-158-gh-watch-summary-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: gh_watch_summary on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level gh_watch_summary on Defer Watch Poll stderr (plan 158) + +## Summary + +Plan 129 flattened **`gh_watch_summary`** to top-level gate JSON and poll stderr already prints **`gh_watch=`**, but only from **`_build_gh_watch_from_status`** before briefing apply. Deferred poll lines should source **`gh_watch=`** from the top-level mirror after **`_apply_lfg_agent_briefing`** for parity with strict exit and watch summary one-liners. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`gh_watch=`** from top-level **`gh_watch_summary`** after briefing apply when deferred. +- R2. Skip pre-briefing **`_build_gh_watch_from_status`** token when **`lfg_deferred`** to avoid duplicates. +- R3. Tests; **`PLAN_TRACK_CAP`** 158; closeout doc bullet; plans index **019–158**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`gh_watch=verify:1,fc:2`** exactly once when deferred. +- T2. Gate watch poll stderr includes the same token exactly once. +- T3. Plan patch expects **`019–158`**. diff --git a/docs/plans/2026-05-24-159-queue-flags-watch-poll-plan.md b/docs/plans/2026-05-24-159-queue-flags-watch-poll-plan.md new file mode 100644 index 000000000..89aab17ed --- /dev/null +++ b/docs/plans/2026-05-24-159-queue-flags-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: queue flags on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level Queue Flags on Defer Watch Poll stderr (plan 159) + +## Summary + +Plan 141 flattened **`max_queued_hours`**, **`queue_backlog`**, **`queue_backlog_warning`**, and **`queue_backlog_severe`** to top-level gate JSON. Deferred watch poll stderr still derives **`queued=`** / **`queue_backlog=`** / **`queue_warn=`** from **`_build_defer_queue_context`** before briefing apply instead of the top-level mirror. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`queued=`** / queue flags from top-level flattened fields after **`_apply_lfg_agent_briefing`** when deferred. +- R2. Skip pre-briefing **`_build_defer_queue_context`** queue tokens when **`lfg_deferred`** to avoid duplicates. +- R3. Tests; **`PLAN_TRACK_CAP`** 159; closeout doc bullet; plans index **019–159**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`queued=2.5h`** and **`queue_warn=true`** exactly once when deferred with warning-level backlog. +- T2. Gate watch poll stderr includes **`queue_backlog=true`** exactly once when deferred with severe backlog. +- T3. Plan patch expects **`019–159`**. diff --git a/docs/plans/2026-05-24-160-active-runs-watch-poll-plan.md b/docs/plans/2026-05-24-160-active-runs-watch-poll-plan.md new file mode 100644 index 000000000..341ec8ddd --- /dev/null +++ b/docs/plans/2026-05-24-160-active-runs-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: active_runs on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level active_runs on Defer Watch Poll stderr (plan 160) + +## Summary + +Plan 130 flattened **`active_runs`** to top-level gate JSON and strict exit already emits **`active_runs=`**. Deferred watch poll stderr still derives **`active_runs=`** from **`_build_active_runs_list`** before briefing apply instead of the top-level mirror. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`active_runs=`** from top-level **`active_runs`** after **`_apply_lfg_agent_briefing`** when deferred. +- R2. Skip pre-briefing **`_build_active_runs_list`** token when **`lfg_deferred`** to avoid duplicates. +- R3. Tests; **`PLAN_TRACK_CAP`** 160; closeout doc bullet; plans index **019–160**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`active_runs=verify,fc`** exactly once when deferred. +- T2. Gate watch poll stderr includes the same token exactly once. +- T3. Plan patch expects **`019–160`**. diff --git a/docs/plans/2026-05-24-161-run-url-watch-poll-plan.md b/docs/plans/2026-05-24-161-run-url-watch-poll-plan.md new file mode 100644 index 000000000..f5ea46b10 --- /dev/null +++ b/docs/plans/2026-05-24-161-run-url-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: run URLs on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level Run URLs on Defer Watch Poll stderr (plan 161) + +## Summary + +Plan 138 flattened **`verify_run_url`** / **`fc_run_url`** to top-level gate JSON and watch summary JSON. Deferred watch poll stderr still omits URL tokens even though run IDs are mirrored (plan 156). + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits truncated **`verify_run_url=`** / **`fc_run_url=`** from top-level mirrors after **`_apply_lfg_agent_briefing`** when deferred. +- R2. Reuse stderr truncation helper (96 chars) consistent with **`briefing_command=`** / **`queue_note=`**. +- R3. Tests; **`PLAN_TRACK_CAP`** 161; closeout doc bullet; plans index **019–161**. + +--- + +## Test scenarios + +- T1. Preflight watch poll stderr includes **`verify_run_url=`** and **`fc_run_url=`** when deferred and runs are active. +- T2. Gate watch poll stderr includes the same URL tokens. +- T3. Plan patch expects **`019–161`**. diff --git a/docs/plans/2026-05-24-162-skip-legacy-run-ids-watch-poll-plan.md b/docs/plans/2026-05-24-162-skip-legacy-run-ids-watch-poll-plan.md new file mode 100644 index 000000000..054bfe97e --- /dev/null +++ b/docs/plans/2026-05-24-162-skip-legacy-run-ids-watch-poll-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: skip legacy run ids on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Skip Legacy verify=/fc= on Defer Watch Poll stderr (plan 162) + +## Summary + +Plan 156 mirrored **`verify_run=`** / **`fc_run=`** from top-level gate JSON on deferred watch poll stderr. The pre-briefing loop still emits legacy **`verify=`** / **`fc=`** run ID tokens, duplicating the canonical fields on every deferred poll line. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** skips legacy **`verify=`** / **`fc=`** run ID tokens when **`lfg_deferred`** (same gate as plan 157 status dedupe). +- R2. Non-deferred poll lines keep legacy **`verify=`** / **`fc=`** for backward compatibility. +- R3. Tests; **`PLAN_TRACK_CAP`** 162; closeout doc bullet; plans index **019–162**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`verify_run=`** / **`fc_run=`** exactly once each and omits legacy **`verify=`** / **`fc=`** tokens. +- T2. Gate watch poll stderr matches the same dedupe behavior. +- T3. Non-deferred poll fixture still emits legacy **`verify=`** / **`fc=`** when applicable. +- T4. Plan patch expects **`019–162`**. diff --git a/docs/plans/2026-05-24-163-skip-per-run-queued-watch-poll-plan.md b/docs/plans/2026-05-24-163-skip-per-run-queued-watch-poll-plan.md new file mode 100644 index 000000000..7331717c3 --- /dev/null +++ b/docs/plans/2026-05-24-163-skip-per-run-queued-watch-poll-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: skip per-run queued on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Skip Per-Run verify_queued/fc_queued on Defer Watch Poll stderr (plan 163) + +## Summary + +Plan 159 mirrored aggregate **`queued=`** / queue flags from top-level gate JSON on deferred watch poll stderr. The pre-briefing loop still emits **`verify_queued=`** / **`fc_queued=`** per-run tokens, duplicating the canonical aggregate on every deferred poll line. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** skips **`verify_queued=`** / **`fc_queued=`** when **`lfg_deferred`**. +- R2. Non-deferred poll lines keep per-run queued tokens for backward compatibility. +- R3. Tests; **`PLAN_TRACK_CAP`** 163; closeout doc bullet; plans index **019–163**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`queued=1.5h`** exactly once and omits **`verify_queued=`** / **`fc_queued=`**. +- T2. Gate watch poll stderr matches the same dedupe behavior. +- T3. Non-deferred poll fixture still emits **`verify_queued=`** / **`fc_queued=`**. +- T4. Plan patch expects **`019–163`**. diff --git a/docs/plans/2026-05-24-164-sha-gap-short-watch-poll-plan.md b/docs/plans/2026-05-24-164-sha-gap-short-watch-poll-plan.md new file mode 100644 index 000000000..12e399187 --- /dev/null +++ b/docs/plans/2026-05-24-164-sha-gap-short-watch-poll-plan.md @@ -0,0 +1,31 @@ +--- +title: "fix: top-level sha_gap on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level sha_gap_short on Defer Watch Poll stderr (plan 164) + +## Summary + +Plan 147 flattened **`sha_gap_short`** to top-level gate JSON. Deferred watch poll stderr still emits a pre-briefing checkpoint **`sha_gap=`** from raw SHAs and a second **`sha_gap=`** from briefing after apply, duplicating or drifting from the top-level mirror. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** skips pre-briefing checkpoint **`sha_gap=`** when **`lfg_deferred`**. +- R2. Emit **`sha_gap=`** from top-level **`sha_gap_short`** after **`_apply_lfg_agent_briefing`** when deferred. +- R3. Non-deferred poll lines keep pre-briefing checkpoint **`sha_gap=`** for backward compatibility. +- R4. Tests; **`PLAN_TRACK_CAP`** 164; closeout doc bullet; plans index **019–164**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`sha_gap=7d85438:8916e2f`** exactly once. +- T2. Gate watch poll stderr matches the same dedupe behavior. +- T3. Non-deferred poll fixture still emits checkpoint **`sha_gap=`**. +- T4. Plan patch expects **`019–164`**. diff --git a/docs/plans/2026-05-24-165-primary-action-watch-poll-plan.md b/docs/plans/2026-05-24-165-primary-action-watch-poll-plan.md new file mode 100644 index 000000000..ecae08fcd --- /dev/null +++ b/docs/plans/2026-05-24-165-primary-action-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level primary_action on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level primary_action / expected_after on Defer Watch Poll stderr (plan 165) + +## Summary + +Plan 132 flattened **`primary_action`** and **`expected_after_terminal`** to top-level gate JSON. Deferred watch poll stderr still reads both from the nested **`lfg_agent_briefing`** dict instead of the top-level mirrors after **`_apply_lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`primary_action=`** from top-level **`primary_action`** after briefing apply when deferred. +- R2. Emit **`expected_after=`** from top-level **`expected_after_terminal.action`** after briefing apply when deferred. +- R3. Tests; **`PLAN_TRACK_CAP`** 165; closeout doc bullet; plans index **019–165**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`primary_action=gate_watch`** and **`expected_after=closeout`** from top-level mirrors. +- T2. Gate watch poll stderr includes the same tokens. +- T3. Plan patch expects **`019–165`**. diff --git a/docs/plans/2026-05-24-166-watch-commands-watch-poll-plan.md b/docs/plans/2026-05-24-166-watch-commands-watch-poll-plan.md new file mode 100644 index 000000000..2ccffc895 --- /dev/null +++ b/docs/plans/2026-05-24-166-watch-commands-watch-poll-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level watch commands on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level gh_watch_command / briefing_command on Defer Watch Poll stderr (plan 166) + +## Summary + +Plans 148–149 flattened **`gh_watch_command`** and **`briefing_command`** to top-level gate JSON. Deferred watch poll stderr still derives **`watch=`** via **`_extract_gh_watch_command(briefing)`** and **`briefing_command=`** from **`briefing.command`** instead of the top-level mirrors after **`_apply_lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`watch=`** from top-level **`gh_watch_command`** after briefing apply when deferred. +- R2. Emit truncated **`briefing_command=`** from top-level **`briefing_command`** after briefing apply when deferred. +- R3. Tests; **`PLAN_TRACK_CAP`** 166; closeout doc bullet; plans index **019–166**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`watch=gh run watch …`** and **`briefing_command=`** from top-level mirrors exactly once each. +- T2. Gate watch poll stderr matches the same behavior. +- T3. Plan patch expects **`019–166`**. diff --git a/docs/plans/2026-05-24-167-notes-merge-ready-watch-poll-plan.md b/docs/plans/2026-05-24-167-notes-merge-ready-watch-poll-plan.md new file mode 100644 index 000000000..47cc58192 --- /dev/null +++ b/docs/plans/2026-05-24-167-notes-merge-ready-watch-poll-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: top-level notes and merge_ready on defer watch poll stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level briefing_notes / briefing_merge_ready on Defer Watch Poll stderr (plan 167) + +## Summary + +Plans 143–145 flattened **`briefing_notes`** and **`briefing_merge_ready`** to top-level gate JSON. Deferred watch poll stderr still derives **`notes=N`** and **`merge_ready=`** from nested **`lfg_agent_briefing`** helpers instead of the top-level mirrors after **`_apply_lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`notes=N`** from top-level **`briefing_notes`** after briefing apply when deferred. +- R2. Emit **`merge_ready=`** from top-level **`briefing_merge_ready`** after briefing apply when deferred. +- R3. Remove unused nested briefing reads from the deferred poll formatter when no longer needed. +- R4. Tests; **`PLAN_TRACK_CAP`** 167; closeout doc bullet; plans index **019–167**. + +--- + +## Test scenarios + +- T1. Deferred preflight watch poll stderr includes **`notes=1`** when checkpoint queue note populates top-level **`briefing_notes`**. +- T2. Gate watch poll stderr includes **`merge_ready=false`** from top-level mirror. +- T3. Plan patch expects **`019–167`**. diff --git a/docs/plans/2026-05-24-168-watch-summary-run-refs-plan.md b/docs/plans/2026-05-24-168-watch-summary-run-refs-plan.md new file mode 100644 index 000000000..aa3135d22 --- /dev/null +++ b/docs/plans/2026-05-24-168-watch-summary-run-refs-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: run refs on watch summary one-liner stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Run IDs and URLs on Watch Summary One-Liner stderr (plan 168) + +## Summary + +Plans 137–138 mirror **`verify_run_id`** / **`fc_run_id`** and run URLs into **`preflight_watch_summary`** JSON. The watch summary one-liner stderr omits **`verify_run=`** / **`fc_run=`** and truncated run URL tokens that deferred poll stderr already emits (plans 156–161). + +--- + +## Requirements + +- R1. **`_format_preflight_watch_summary_line`** emits **`verify_run=`** / **`fc_run=`** when summary includes run IDs. +- R2. Emit truncated **`verify_run_url=`** / **`fc_run_url=`** using **`_format_run_url_stderr`**. +- R3. Tests; **`PLAN_TRACK_CAP`** 168; closeout doc bullet; plans index **019–168**. + +--- + +## Test scenarios + +- T1. Watch summary one-liner includes run ID and URL tokens when summary JSON carries them. +- T2. Gate watch summary one-liner includes the same tokens. +- T3. Plan patch expects **`019–168`**. diff --git a/docs/plans/2026-05-24-169-watch-summary-queue-top-level-plan.md b/docs/plans/2026-05-24-169-watch-summary-queue-top-level-plan.md new file mode 100644 index 000000000..3a3809be7 --- /dev/null +++ b/docs/plans/2026-05-24-169-watch-summary-queue-top-level-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level queue flags on watch summary stderr" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level Queue Fields on Watch Summary One-Liner stderr (plan 169) + +## Summary + +Plan 141 flattened queue backlog fields to top-level **`preflight_watch_summary`** JSON via **`_mirror_queue_context_fields`**. The watch summary one-liner stderr still reads **`queued=`** / queue flags only from nested **`queue_context`**, diverging from deferred poll stderr (plan 159) and top-level gate JSON. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_summary_line`** emits **`queued=`** / queue flags from top-level **`max_queued_hours`** / backlog flags when present. +- R2. Fall back to nested **`queue_context`** only when top-level queue fields are absent (direct formatter tests). +- R3. Tests; **`PLAN_TRACK_CAP`** 169; closeout doc bullet; plans index **019–169**. + +--- + +## Test scenarios + +- T1. Watch summary one-liner prefers top-level **`max_queued_hours`** / **`queue_backlog_warning`** over nested **`queue_context`**. +- T2. Formatter still works when only nested **`queue_context`** is supplied. +- T3. Plan patch expects **`019–169`**. diff --git a/docs/plans/2026-05-24-170-watch-summary-status-mirror-plan.md b/docs/plans/2026-05-24-170-watch-summary-status-mirror-plan.md new file mode 100644 index 000000000..ee41af821 --- /dev/null +++ b/docs/plans/2026-05-24-170-watch-summary-status-mirror-plan.md @@ -0,0 +1,38 @@ +--- +title: "fix: mirror watch summary from top-level status" +type: fix +status: completed +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Mirror Preflight Watch Summary from Top-Level status (plan 170) + +## Summary + +`_apply_lfg_agent_briefing` already flattens defer briefing fields onto top-level **`status`** (plans 129–167). **`_watch_lfg_preflight_defer`** still copies the same fields from nested **`lfg_agent_briefing`** into **`preflight_watch_summary`**, diverging from gate JSON and deferred poll stderr which read **`status`** after apply. + +--- + +## Requirements + +- R1. Add **`_mirror_preflight_watch_summary_from_status(status, summary)`** that copies briefing mirrors from top-level **`status`**. +- R2. **`_watch_lfg_preflight_defer`** calls **`_apply_lfg_agent_briefing`** then the helper when deferred; remove duplicate briefing→summary copies. +- R3. **`active_runs`** / **`gh_watch_summary`** on summary prefer top-level **`status`** after briefing apply. +- R4. Tests; **`PLAN_TRACK_CAP`** 170; closeout bullet; plans index **019–170**. + +--- + +## Implementation Units + +- U1. Helper + watch defer refactor in `.github/scripts/local_verify_pypi_slice.py` +- U2. Tests in `Libraries/PyKotor/tests/test_utility/test_local_verify_checkpoint.py` +- U3. Closeout doc + plan index + +--- + +## Test scenarios + +- T1. Helper copies **`primary_action`**, **`verify_run_id`**, **`briefing_action`** from status onto summary. +- T2. Deferred watch timeout: **`preflight_watch_summary`** includes top-level mirrored **`primary_action=gate_watch`** (patched defer path). +- T3. Plan patch expects **`019–170`**. diff --git a/docs/plans/2026-05-24-171-strict-exit-status-mirror-plan.md b/docs/plans/2026-05-24-171-strict-exit-status-mirror-plan.md new file mode 100644 index 000000000..0aaff7dbc --- /dev/null +++ b/docs/plans/2026-05-24-171-strict-exit-status-mirror-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: strict exit stderr from top-level status" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Strict Exit stderr from Top-Level status (plan 171) + +## Summary + +`_apply_lfg_agent_briefing` runs before `_emit_lfg_strict_exit_stderr`, flattening defer mirrors onto top-level **`status`**. Strict exit stderr still reads nested **`lfg_agent_briefing`**, diverging from deferred poll stderr (plans 165–167) and watch summary (plan 170). + +--- + +## Requirements + +- R1. Extract **`_lfg_briefing_mirror_stderr_parts(status)`** shared by deferred poll stderr and strict exit. +- R2. **`_emit_lfg_strict_exit_stderr`** appends tokens from top-level **`status`**, with briefing fallback for direct unit tests. +- R3. Tests; **`PLAN_TRACK_CAP`** 171; closeout bullet; plans index **019–171**. + +--- + +## Test scenarios + +- T1. Strict exit prefers top-level **`primary_action`** / **`max_queued_hours`** over nested briefing when both present. +- T2. Existing defer briefing strict-exit tests still pass via briefing fallback. +- T3. Plan patch expects **`019–171`**. diff --git a/docs/plans/2026-05-24-172-watch-summary-shared-helper-plan.md b/docs/plans/2026-05-24-172-watch-summary-shared-helper-plan.md new file mode 100644 index 000000000..202236650 --- /dev/null +++ b/docs/plans/2026-05-24-172-watch-summary-shared-helper-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: watch summary line uses shared mirror helper" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Watch Summary One-Liner Uses Shared Mirror Helper (plan 172) + +## Summary + +Plans 169–171 unified defer stderr tokens via **`_lfg_briefing_mirror_stderr_parts`**. **`_format_preflight_watch_summary_line`** still duplicates the same field reads. Refactor it to append shared mirror parts after watch-specific prefix tokens. + +--- + +## Requirements + +- R1. Extend helper queue fallback to read nested **`queue_context`** on the target dict (for direct formatter tests). +- R2. **`_format_preflight_watch_summary_line`** uses **`_lfg_briefing_mirror_stderr_parts(summary)`** after **`result=`** / **`next=`** prefix tokens. +- R3. Tests; **`PLAN_TRACK_CAP`** 172; closeout bullet; plans index **019–172**. + +--- + +## Test scenarios + +- T1. Watch summary line still emits **`queued=`** when only nested **`queue_context`** is supplied. +- T2. Existing watch summary formatter tests pass. +- T3. Plan patch expects **`019–172`**. diff --git a/docs/plans/2026-05-24-173-briefing-stderr-status-mirror-plan.md b/docs/plans/2026-05-24-173-briefing-stderr-status-mirror-plan.md new file mode 100644 index 000000000..65d7091ea --- /dev/null +++ b/docs/plans/2026-05-24-173-briefing-stderr-status-mirror-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: briefing stderr from top-level status" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Briefing stderr from Top-Level status (plan 173) + +## Summary + +Poll, strict-exit, and watch-summary stderr use **`_lfg_briefing_mirror_stderr_parts(status)`** (plans 171–172). **`_emit_lfg_agent_briefing_stderr`** still reads nested briefing only. After **`_apply_lfg_agent_briefing`**, call sites should pass **`status`** and reuse the shared helper for mirror tokens. + +--- + +## Requirements + +- R1. **`_emit_lfg_agent_briefing_stderr(status)`** appends mirror parts from top-level **`status`** (briefing fallback). +- R2. Preserve briefing-specific tokens: **`reason=`** (defer), **`wait=`**, **`drift_fields=`**, **`exit=`**, **`complete=`**. +- R3. Call sites pass **`status`** after apply; tests; **`PLAN_TRACK_CAP`** 173; closeout bullet; plans index **019–173**. + +--- + +## Test scenarios + +- T1. Briefing stderr prefers top-level **`verify_run_id`** over nested briefing when both present. +- T2. Defer briefing still emits **`reason=`** (not only **`briefing_reason=`**). +- T3. Existing drift/defer/track-complete briefing tests pass. diff --git a/docs/plans/2026-05-24-174-wait-drift-top-level-plan.md b/docs/plans/2026-05-24-174-wait-drift-top-level-plan.md new file mode 100644 index 000000000..e14ba961f --- /dev/null +++ b/docs/plans/2026-05-24-174-wait-drift-top-level-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: top-level wait_recommended and ci_drift mirrors" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Top-Level wait_recommended and ci_drift Mirrors (plan 174) + +## Summary + +`investigate_ci_drift` briefing carries **`wait_recommended`** and **`drift`**, but **`_apply_lfg_agent_briefing`** does not flatten them to top-level **`status`** / gate JSON. Briefing stderr reads nested briefing; agents polling **`--lfg-gate --json`** cannot see wait/drift without opening **`lfg_agent_briefing`**. + +--- + +## Requirements + +- R1. **`_apply_lfg_agent_briefing`** mirrors **`wait_recommended`** and **`ci_drift`** onto top-level **`status`**. +- R2. **`_mirror_preflight_watch_summary_from_status`** copies both into **`preflight_watch_summary`** JSON. +- R3. Tests; **`PLAN_TRACK_CAP`** 174; closeout bullet; plans index **019–174**. + +--- + +## Test scenarios + +- T1. Drift path with active FC → top-level **`wait_recommended`** and **`ci_drift`** on status after apply. +- T2. Deferred watch summary JSON includes mirrored **`wait_recommended`** / **`ci_drift`**. +- T3. Plan patch expects **`019–174`**. diff --git a/docs/plans/2026-05-24-175-mirror-wait-drift-stderr-plan.md b/docs/plans/2026-05-24-175-mirror-wait-drift-stderr-plan.md new file mode 100644 index 000000000..c2b0fca05 --- /dev/null +++ b/docs/plans/2026-05-24-175-mirror-wait-drift-stderr-plan.md @@ -0,0 +1,30 @@ +--- +title: "fix: mirror wait and drift_fields stderr tokens" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Mirror wait and drift_fields Stderr Tokens (plan 175) + +## Summary + +Plan 174 flattened **`wait_recommended`** and **`ci_drift`** onto top-level **`status`**. Poll, strict-exit, and watch-summary stderr still omit **`wait=true`** / **`drift_fields=`** because those tokens lived only in **`_emit_lfg_agent_briefing_stderr`**. Move them into **`_lfg_briefing_mirror_stderr_parts`** and DRY briefing emit. + +--- + +## Requirements + +- R1. **`_lfg_briefing_mirror_stderr_parts`** emits **`wait=true`** when action is **`investigate_ci_drift`** and **`wait_recommended`** (top-level or briefing fallback). +- R2. Same helper emits **`drift_fields=`** from top-level **`ci_drift`** or nested **`drift`**. +- R3. **`_emit_lfg_agent_briefing_stderr`** drops duplicate wait/drift logic; keeps defer **`reason=`**, **`exit=`**, **`complete=`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 175; closeout bullet; plans index **019–175**. + +--- + +## Test scenarios + +- T1. Mirror helper on status with top-level **`wait_recommended`** / **`ci_drift`** → **`wait=true`** and **`drift_fields=`**. +- T2. Strict exit stderr includes both when deferred investigate-drift briefing is applied to status. +- T3. Existing briefing drift/defer stderr tests pass; plan patch expects **`019–175`**. diff --git a/docs/plans/2026-05-24-176-shared-flat-field-mirror-plan.md b/docs/plans/2026-05-24-176-shared-flat-field-mirror-plan.md new file mode 100644 index 000000000..b1f89af2e --- /dev/null +++ b/docs/plans/2026-05-24-176-shared-flat-field-mirror-plan.md @@ -0,0 +1,30 @@ +--- +title: "refactor: shared lfg flat field mirror helper" +type: refactor +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Shared LFG Flat Field Mirror Helper (plan 176) + +## Summary + +**`_apply_lfg_agent_briefing`** and **`_mirror_preflight_watch_summary_from_status`** duplicate the same flattened field copies (run refs, commands, drift, queue mirrors). Extract **`_mirror_lfg_flat_fields`** so both paths stay aligned. + +--- + +## Requirements + +- R1. **`_mirror_lfg_flat_fields(source, target, *, clear_missing, queue_context_filter)`** copies shared flat keys; accepts briefing-shaped or status-shaped **`source`** (action/reason/drift aliases). +- R2. **`_apply_lfg_agent_briefing`** uses helper with **`clear_missing=True`**; watch summary uses **`queue_context_filter=True`**. +- R3. Remove duplicate **`watch_recommended`** copy in watch summary mirror. +- R4. Tests; **`PLAN_TRACK_CAP`** 176; closeout bullet; plans index **019–176**. + +--- + +## Test scenarios + +- T1. Direct helper test: briefing-shaped source → flat target with run refs and drift. +- T2. Existing **`test_mirror_preflight_watch_summary_from_status`** and apply drift tests pass. +- T3. Plan patch expects **`019–176`**. diff --git a/docs/plans/2026-05-24-177-lfg-flat-field-keys-plan.md b/docs/plans/2026-05-24-177-lfg-flat-field-keys-plan.md new file mode 100644 index 000000000..22b554000 --- /dev/null +++ b/docs/plans/2026-05-24-177-lfg-flat-field-keys-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: lfg_flat_field_keys in gate JSON" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: lfg_flat_field_keys in Gate JSON (plan 177) + +## Summary + +Plans 174–176 flattened briefing onto top-level **`status`** and shared **`_mirror_lfg_flat_fields`**. Agents polling **`--lfg-gate --json`** still had to guess which keys to read. Export **`LFG_FLAT_FIELD_KEYS`** as **`lfg_flat_field_keys`** on status (and watch summary) after apply. + +--- + +## Requirements + +- R1. Module-level **`LFG_FLAT_FIELD_KEYS`** tuple documents all flattened top-level keys. +- R2. **`_apply_lfg_agent_briefing`** sets **`lfg_flat_field_keys`** when briefing exists; pops when cleared. +- R3. **`preflight_watch_summary`** copies **`lfg_flat_field_keys`** from status after mirror. +- R4. Tests; **`PLAN_TRACK_CAP`** 177; closeout bullet; plans index **019–177**. + +--- + +## Test scenarios + +- T1. Apply briefing → status includes **`lfg_flat_field_keys`** matching **`LFG_FLAT_FIELD_KEYS`**. +- T2. Watch summary mirror copies **`lfg_flat_field_keys`** from status. +- T3. Plan patch expects **`019–177`**. diff --git a/docs/plans/2026-05-24-178-lfg-flat-field-values-plan.md b/docs/plans/2026-05-24-178-lfg-flat-field-values-plan.md new file mode 100644 index 000000000..cb2c0b6e8 --- /dev/null +++ b/docs/plans/2026-05-24-178-lfg-flat-field-values-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: lfg_flat_field_values compact gate JSON" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: lfg_flat_field_values Compact Gate JSON (plan 178) + +## Summary + +Plan 177 exported **`lfg_flat_field_keys`** as a legend. Agents still scan many top-level keys. Add **`lfg_flat_field_values`** — a dict of only populated flattened fields — on status and preflight watch summary after apply/mirror. + +--- + +## Requirements + +- R1. **`_build_lfg_flat_field_values(status)`** collects non-empty values for keys in **`LFG_FLAT_FIELD_KEYS`** (bools included when present). +- R2. **`_apply_lfg_agent_briefing`** sets **`lfg_flat_field_values`** when non-empty; pops when cleared. +- R3. **`_mirror_preflight_watch_summary_from_status`** rebuilds values from mirrored summary fields. +- R4. Tests; **`PLAN_TRACK_CAP`** 178; closeout bullet; plans index **019–178**. + +--- + +## Test scenarios + +- T1. Apply drift briefing → **`lfg_flat_field_values`** includes **`wait_recommended`**, **`ci_drift`**, run refs; omits unset keys. +- T2. Watch summary mirror includes compact values dict. +- T3. Plan patch expects **`019–178`**. diff --git a/docs/plans/2026-05-24-179-flat-fields-stderr-plan.md b/docs/plans/2026-05-24-179-flat-fields-stderr-plan.md new file mode 100644 index 000000000..5e254edae --- /dev/null +++ b/docs/plans/2026-05-24-179-flat-fields-stderr-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: flat_fields stderr token for poll scans" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_fields Stderr Token for Poll Scans (plan 179) + +## Summary + +Plan 178 added **`lfg_flat_field_values`** JSON. Poll and strict-exit stderr still lack a quick populated-field count. Add **`flat_fields=N`** to **`_lfg_briefing_mirror_stderr_parts`** (prefers cached **`lfg_flat_field_values`**, else builds count inline). + +--- + +## Requirements + +- R1. **`_lfg_flat_field_stderr_count(status)`** returns len of cached values or **`_build_lfg_flat_field_values`**. +- R2. Mirror stderr appends **`flat_fields=N`** when **N > 0**. +- R3. Poll, strict-exit, watch-summary, and briefing stderr inherit via shared helper. +- R4. Tests; **`PLAN_TRACK_CAP`** 179; closeout bullet; plans index **019–179**. + +--- + +## Test scenarios + +- T1. Mirror helper with **`lfg_flat_field_values`** → **`flat_fields=3`** (example count). +- T2. Strict exit / deferred poll stderr includes **`flat_fields=`** after apply-shaped status. +- T3. Plan patch expects **`019–179`**. diff --git a/docs/plans/2026-05-24-180-strict-exit-flat-fields-plan.md b/docs/plans/2026-05-24-180-strict-exit-flat-fields-plan.md new file mode 100644 index 000000000..026218ab0 --- /dev/null +++ b/docs/plans/2026-05-24-180-strict-exit-flat-fields-plan.md @@ -0,0 +1,29 @@ +--- +title: "fix: strict-exit stderr without nested briefing" +type: fix +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# fix: Strict-Exit Stderr Without Nested Briefing (plan 180) + +## Summary + +**`_emit_lfg_strict_exit_stderr`** only appended mirror tokens when **`lfg_agent_briefing`** was present. After plans 174–179, top-level flat fields and **`lfg_flat_field_values`** may exist without nested briefing. Emit mirror stderr when flat fields are populated. + +--- + +## Requirements + +- R1. **`_should_attach_lfg_mirror_stderr(status)`** true when nested briefing exists or flat-field count **> 0**. +- R2. **`_emit_lfg_strict_exit_stderr`** uses helper instead of briefing-only guard. +- R3. Tests; **`PLAN_TRACK_CAP`** 180; closeout bullet; plans index **019–180**. + +--- + +## Test scenarios + +- T1. Strict exit with top-level flat fields, no **`lfg_agent_briefing`** → mirror tokens including **`flat_fields=`**. +- T2. Existing strict-exit briefing tests unchanged. +- T3. Plan patch expects **`019–180`**. diff --git a/docs/plans/2026-05-24-181-flat-field-keys-present-plan.md b/docs/plans/2026-05-24-181-flat-field-keys-present-plan.md new file mode 100644 index 000000000..7b5eab900 --- /dev/null +++ b/docs/plans/2026-05-24-181-flat-field-keys-present-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: lfg_flat_field_keys_present in gate JSON" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: lfg_flat_field_keys_present in Gate JSON (plan 181) + +## Summary + +Plan 178 added **`lfg_flat_field_values`** (full compact dict). Agents iterating keys still pay dict size cost. Add **`lfg_flat_field_keys_present`** — ordered key list of populated flat fields only — on status and preflight watch summary. + +--- + +## Requirements + +- R1. **`_build_lfg_flat_field_keys_present(flat_values)`** preserves **`LFG_FLAT_FIELD_KEYS`** order. +- R2. **`_apply_lfg_agent_briefing`** sets **`lfg_flat_field_keys_present`** when values exist; pops when cleared. +- R3. Watch summary mirror rebuilds present-keys from mirrored values. +- R4. Tests; **`PLAN_TRACK_CAP`** 181; closeout bullet; plans index **019–181**. + +--- + +## Test scenarios + +- T1. Apply briefing → present-keys list matches populated subset in canonical order. +- T2. Watch summary includes present-keys after mirror. +- T3. Plan patch expects **`019–181`**. diff --git a/docs/plans/2026-05-24-182-flat-keys-stderr-plan.md b/docs/plans/2026-05-24-182-flat-keys-stderr-plan.md new file mode 100644 index 000000000..cb66ab2c2 --- /dev/null +++ b/docs/plans/2026-05-24-182-flat-keys-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: flat_keys stderr token for poll diffs" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_keys Stderr Token for Poll Diffs (plan 182) + +## Summary + +Plan 181 added **`lfg_flat_field_keys_present`** JSON. Poll stderr has **`flat_fields=N`** count but not which keys changed. Add **`flat_keys=k1,k2,...`** to shared mirror stderr (prefers cached present-keys list). + +--- + +## Requirements + +- R1. **`_lfg_flat_field_keys_present_stderr(status)`** resolves present keys from cache or builds inline. +- R2. Mirror stderr appends **`flat_keys=`** comma list when non-empty (alongside **`flat_fields=N`**). +- R3. Tests; **`PLAN_TRACK_CAP`** 182; closeout bullet; plans index **019–182**. + +--- + +## Test scenarios + +- T1. Mirror helper with **`lfg_flat_field_keys_present`** → **`flat_keys=primary_action,fc_run_id`**. +- T2. Strict-exit stderr includes **`flat_keys=`** when top-level flat fields populated. +- T3. Plan patch expects **`019–182`**. diff --git a/docs/plans/2026-05-24-183-flat-keys-unchanged-poll-plan.md b/docs/plans/2026-05-24-183-flat-keys-unchanged-poll-plan.md new file mode 100644 index 000000000..2b622d000 --- /dev/null +++ b/docs/plans/2026-05-24-183-flat-keys-unchanged-poll-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: omit unchanged flat_keys on gate-watch polls" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: Omit Unchanged flat_keys on Gate-Watch Polls (plan 183) + +## Summary + +Plan 182 added **`flat_keys=`** stderr on every deferred poll. When populated keys are unchanged poll-to-poll, omit **`flat_keys=`** and **`flat_fields=`** and emit **`flat_unchanged=true`** instead. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line(..., previous_flat_keys=)`** compares present-keys to prior poll. +- R2. When equal and non-empty, strip **`flat_keys=`** / **`flat_fields=`** and append **`flat_unchanged=true`**. +- R3. **`_watch_lfg_preflight_defer`** tracks previous flat keys between polls. +- R4. Tests; **`PLAN_TRACK_CAP`** 183; closeout bullet; plans index **019–183**. + +--- + +## Test scenarios + +- T1. Second poll with same present-keys → no **`flat_keys=`**, has **`flat_unchanged=true`**. +- T2. Poll with changed present-keys → full **`flat_keys=`** list, no **`flat_unchanged=true`**. +- T3. Plan patch expects **`019–183`**. diff --git a/docs/plans/2026-05-24-184-unchanged-flat-keys-polls-plan.md b/docs/plans/2026-05-24-184-unchanged-flat-keys-polls-plan.md new file mode 100644 index 000000000..1cff35c3c --- /dev/null +++ b/docs/plans/2026-05-24-184-unchanged-flat-keys-polls-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: unchanged_flat_keys_polls in watch summary" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: unchanged_flat_keys_polls in Watch Summary (plan 184) + +## Summary + +Plan 183 omitted unchanged **`flat_keys=`** on consecutive gate-watch polls. Add **`flat_keys`** to preflight watch history snapshots and **`unchanged_flat_keys_polls`** on **`preflight_watch_summary`** JSON + summary stderr. + +--- + +## Requirements + +- R1. Deferred poll snapshots record **`flat_keys`** list after apply. +- R2. **`_count_unchanged_preflight_flat_keys_polls(history)`** counts consecutive equal **`flat_keys`** pairs. +- R3. **`preflight_watch_summary`** and summary stderr include **`unchanged_flat_keys_polls`** when **> 0**. +- R4. Tests; **`PLAN_TRACK_CAP`** 184; closeout bullet; plans index **019–184**. + +--- + +## Test scenarios + +- T1. History with two polls same **`flat_keys`** → count **1**. +- T2. Summary line includes **`unchanged_flat_keys_polls=1`** when applicable. +- T3. Plan patch expects **`019–184`**. diff --git a/docs/plans/2026-05-24-185-flat-keys-heartbeat-plan.md b/docs/plans/2026-05-24-185-flat-keys-heartbeat-plan.md new file mode 100644 index 000000000..b56b772da --- /dev/null +++ b/docs/plans/2026-05-24-185-flat-keys-heartbeat-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: flat_keys heartbeat on gate-watch polls" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_keys Heartbeat on Gate-Watch Polls (plan 185) + +## Summary + +Plan 183–184 compact unchanged **`flat_keys=`** stderr. Reuse **`--watch-heartbeat-polls`** (default 12) so every N unchanged flat-key polls re-emit full **`flat_keys=`** / **`flat_fields=`** with **`flat_keys_heartbeat=1`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** accepts streak + heartbeat interval; skips compact omit on heartbeat polls. +- R2. **`_watch_lfg_preflight_defer`** tracks flat-key unchanged streak and **`preflight_flat_keys_heartbeats`** count. +- R3. **`preflight_watch_summary`** + summary stderr include **`flat_keys_heartbeat_polls`** when **> 0**. +- R4. Tests; **`PLAN_TRACK_CAP`** 185; closeout bullet; plans index **019–185**. + +--- + +## Test scenarios + +- T1. Streak 12 + heartbeat 12 → **`flat_keys=`** present, **`flat_keys_heartbeat=1`**, no **`flat_unchanged=true`**. +- T2. Streak 1 unchanged → compact omit still applies. +- T3. Plan patch expects **`019–185`**. diff --git a/docs/plans/2026-05-24-186-mirror-flat-fields-relocate-plan.md b/docs/plans/2026-05-24-186-mirror-flat-fields-relocate-plan.md new file mode 100644 index 000000000..5b9cc90a1 --- /dev/null +++ b/docs/plans/2026-05-24-186-mirror-flat-fields-relocate-plan.md @@ -0,0 +1,28 @@ +--- +title: "refactor: relocate _mirror_lfg_flat_fields" +type: refactor +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Relocate `_mirror_lfg_flat_fields` (plan 186) + +## Summary + +Move **`_mirror_lfg_flat_fields`** from the preflight-watch section to the shared **`_mirror_*`** helper cluster (after **`_mirror_briefing_notes`**) so flat-field mirroring sits with queue/briefing mirror helpers it delegates to. + +--- + +## Requirements + +- R1. No behavior change — pure relocation. +- R2. **`PLAN_TRACK_CAP`** 186; closeout index **019–186**. +- R3. Existing mirror tests still pass. + +--- + +## Test scenarios + +- T1. **`test_mirror_lfg_flat_fields_from_briefing`** unchanged behavior. +- T2. Plan patch expects **`019–186`**. diff --git a/docs/plans/2026-05-24-187-preflight-watch-mirror-relocate-plan.md b/docs/plans/2026-05-24-187-preflight-watch-mirror-relocate-plan.md new file mode 100644 index 000000000..97740c860 --- /dev/null +++ b/docs/plans/2026-05-24-187-preflight-watch-mirror-relocate-plan.md @@ -0,0 +1,28 @@ +--- +title: "refactor: relocate preflight watch summary mirror" +type: refactor +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Relocate `_mirror_preflight_watch_summary_from_status` (plan 187) + +## Summary + +Move **`_mirror_preflight_watch_summary_from_status`** from the preflight-watch section to the flat-field mirror cluster (after **`_build_lfg_flat_field_keys_present`**) alongside **`_mirror_lfg_flat_fields`** and flat-field builders. + +--- + +## Requirements + +- R1. No behavior change — pure relocation. +- R2. **`PLAN_TRACK_CAP`** 187; closeout index **019–187**. +- R3. Existing mirror/watch summary tests still pass. + +--- + +## Test scenarios + +- T1. **`test_mirror_preflight_watch_summary_from_status`** unchanged behavior. +- T2. Plan patch expects **`019–187`**. diff --git a/docs/plans/2026-05-24-188-flat-keys-heartbeat-summary-gate-plan.md b/docs/plans/2026-05-24-188-flat-keys-heartbeat-summary-gate-plan.md new file mode 100644 index 000000000..6fc2f4ba9 --- /dev/null +++ b/docs/plans/2026-05-24-188-flat-keys-heartbeat-summary-gate-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: gate preflight flat-keys heartbeat summary stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: Gate Preflight Flat-Keys Heartbeat Summary Stderr (plan 188) + +## Summary + +Record **`watch_heartbeat_polls`** on **`preflight_watch_summary`** and emit **`flat_keys_heartbeat_polls=`** on the summary one-liner only when unchanged flat-key polls reached the heartbeat interval. Relocate **`_count_unchanged_preflight_flat_keys_polls`** with flat-field helpers. + +--- + +## Requirements + +- R1. Watch loop stores **`preflight_watch_heartbeat_polls`** from **`--watch-heartbeat-polls`**. +- R2. Summary JSON includes **`watch_heartbeat_polls`**; stderr **`flat_keys_heartbeat_polls=`** only when **`unchanged_flat_keys_polls >= watch_heartbeat_polls`** and heartbeats **> 0**. +- R3. Move **`_count_unchanged_preflight_flat_keys_polls`** next to flat-field builders. +- R4. **`PLAN_TRACK_CAP`** 188; closeout index **019–188**. + +--- + +## Test scenarios + +- T1. Summary stderr includes heartbeat count when unchanged meets interval. +- T2. Summary stderr omits heartbeat count when unchanged below interval. +- T3. Plan patch expects **`019–188`**. diff --git a/docs/plans/2026-05-24-189-watch-heartbeat-summary-stderr-plan.md b/docs/plans/2026-05-24-189-watch-heartbeat-summary-stderr-plan.md new file mode 100644 index 000000000..2bc68e07f --- /dev/null +++ b/docs/plans/2026-05-24-189-watch-heartbeat-summary-stderr-plan.md @@ -0,0 +1,28 @@ +--- +title: "feat: watch heartbeat interval on preflight summary stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: Watch Heartbeat Interval on Preflight Summary Stderr (plan 189) + +## Summary + +When preflight/gate watch saw unchanged flat-key polls, emit **`watch_heartbeat_polls=N`** on the summary one-liner so agents see the heartbeat interval alongside **`unchanged_flat_keys_polls=`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_summary_line`** appends **`watch_heartbeat_polls=N`** when **`unchanged_flat_keys_polls > 0`** and **`watch_heartbeat_polls > 0`**. +- R2. Tests; **`PLAN_TRACK_CAP`** 189; closeout index **019–189**. + +--- + +## Test scenarios + +- T1. Unchanged flat polls present → summary stderr includes **`watch_heartbeat_polls=12`**. +- T2. No unchanged flat polls → omit **`watch_heartbeat_polls=`**. +- T3. Plan patch expects **`019–189`**. diff --git a/docs/plans/2026-05-24-190-flat-field-stderr-heartbeat-every-plan.md b/docs/plans/2026-05-24-190-flat-field-stderr-heartbeat-every-plan.md new file mode 100644 index 000000000..31a109b93 --- /dev/null +++ b/docs/plans/2026-05-24-190-flat-field-stderr-heartbeat-every-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: flat-field stderr helper and heartbeat_every poll token" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: Flat-Field Stderr Helper and heartbeat_every Poll Token (plan 190) + +## Summary + +Extract **`_lfg_flat_field_mirror_stderr_parts`** next to flat-field stderr helpers and emit **`heartbeat_every=N`** on gate-watch poll lines when flat keys are unchanged. + +--- + +## Requirements + +- R1. **`_lfg_briefing_mirror_stderr_parts`** delegates flat-field tokens to **`_lfg_flat_field_mirror_stderr_parts`**. +- R2. Unchanged or heartbeat flat-key poll lines append **`heartbeat_every=N`** when interval **> 0**. +- R3. Tests; **`PLAN_TRACK_CAP`** 190; closeout index **019–190**. + +--- + +## Test scenarios + +- T1. Shared helper emits **`flat_fields=`** / **`flat_keys=`**. +- T2. Compact unchanged poll line includes **`heartbeat_every=12`**. +- T3. Plan patch expects **`019–190`**. diff --git a/docs/plans/2026-05-24-191-heartbeat-every-summary-stderr-plan.md b/docs/plans/2026-05-24-191-heartbeat-every-summary-stderr-plan.md new file mode 100644 index 000000000..f6d05f562 --- /dev/null +++ b/docs/plans/2026-05-24-191-heartbeat-every-summary-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: heartbeat_every on preflight summary stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: heartbeat_every on Preflight Summary Stderr (plan 191) + +## Summary + +Align preflight/gate watch **summary** stderr with poll lines: emit compact **`heartbeat_every=N`** (replacing **`watch_heartbeat_polls=`**) when unchanged flat-key polls occurred. JSON keeps **`watch_heartbeat_polls`**. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_summary_line`** emits **`heartbeat_every=N`** when **`unchanged_flat_keys_polls > 0`** and interval **> 0**. +- R2. Omit legacy **`watch_heartbeat_polls=`** summary stderr token. +- R3. Tests; **`PLAN_TRACK_CAP`** 191; closeout index **019–191**. + +--- + +## Test scenarios + +- T1. Unchanged flat polls → summary stderr includes **`heartbeat_every=12`**. +- T2. No unchanged flat polls → omit **`heartbeat_every=`**. +- T3. Plan patch expects **`019–191`**. diff --git a/docs/plans/2026-05-24-192-heartbeat-every-json-flat-hb-plan.md b/docs/plans/2026-05-24-192-heartbeat-every-json-flat-hb-plan.md new file mode 100644 index 000000000..8bb1b5fbf --- /dev/null +++ b/docs/plans/2026-05-24-192-heartbeat-every-json-flat-hb-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: heartbeat_every json alias and flat_hb summary stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: heartbeat_every JSON Alias and flat_hb Summary Stderr (plan 192) + +## Summary + +Add **`heartbeat_every`** to **`preflight_watch_summary`** JSON (alias of **`watch_heartbeat_polls`**) and compact gated summary stderr **`flat_keys_heartbeat_polls=`** to **`flat_hb=`**. + +--- + +## Requirements + +- R1. Summary JSON sets **`heartbeat_every`** when **`watch_heartbeat_polls > 0`**. +- R2. Gated summary stderr emits **`flat_hb=N`** instead of **`flat_keys_heartbeat_polls=N`**. +- R3. Heartbeat gate accepts **`heartbeat_every`** or **`watch_heartbeat_polls`** for interval. +- R4. Tests; **`PLAN_TRACK_CAP`** 192; closeout index **019–192**. + +--- + +## Test scenarios + +- T1. Summary JSON includes **`heartbeat_every`** when interval configured. +- T2. Summary stderr uses **`flat_hb=1`** when heartbeat count gated. +- T3. Plan patch expects **`019–192`**. diff --git a/docs/plans/2026-05-24-193-flat-hb-poll-json-plan.md b/docs/plans/2026-05-24-193-flat-hb-poll-json-plan.md new file mode 100644 index 000000000..3e20f1455 --- /dev/null +++ b/docs/plans/2026-05-24-193-flat-hb-poll-json-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: flat_hb poll stderr and json alias" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_hb Poll Stderr and JSON Alias (plan 193) + +## Summary + +Align gate-watch **poll** stderr with summary: emit **`flat_hb=1`** on heartbeat polls (replacing **`flat_keys_heartbeat=1`**) and add **`flat_hb`** JSON alias on **`preflight_watch_summary`**. + +--- + +## Requirements + +- R1. Heartbeat poll lines emit **`flat_hb=1`** instead of **`flat_keys_heartbeat=1`**. +- R2. Summary JSON sets **`flat_hb`** when **`flat_keys_heartbeat_polls > 0`**. +- R3. Heartbeat gate/formatter resolve count from **`flat_hb`** or **`flat_keys_heartbeat_polls`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 193; closeout index **019–193**. + +--- + +## Test scenarios + +- T1. Heartbeat poll line includes **`flat_hb=1`**, not **`flat_keys_heartbeat=`**. +- T2. Summary JSON includes **`flat_hb`** when heartbeats occurred. +- T3. Plan patch expects **`019–193`**. diff --git a/docs/plans/2026-05-24-194-flat-unchanged-summary-plan.md b/docs/plans/2026-05-24-194-flat-unchanged-summary-plan.md new file mode 100644 index 000000000..0db9a4a10 --- /dev/null +++ b/docs/plans/2026-05-24-194-flat-unchanged-summary-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: flat_unchanged summary stderr and json alias" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_unchanged Summary Stderr and JSON Alias (plan 194) + +## Summary + +Compact preflight/gate watch **summary** stderr: emit **`flat_unchanged=N`** (replacing **`unchanged_flat_keys_polls=`**) and add matching **`flat_unchanged`** JSON alias on **`preflight_watch_summary`**. + +--- + +## Requirements + +- R1. Summary JSON sets **`flat_unchanged`** when **`unchanged_flat_keys_polls > 0`**. +- R2. Summary stderr emits **`flat_unchanged=N`** instead of **`unchanged_flat_keys_polls=`**. +- R3. Heartbeat gate resolves unchanged count via **`flat_unchanged`** or **`unchanged_flat_keys_polls`**. +- R4. Tests; **`PLAN_TRACK_CAP`** 194; closeout index **019–194**. + +--- + +## Test scenarios + +- T1. Summary stderr includes **`flat_unchanged=3`**, not long key. +- T2. Summary JSON includes **`flat_unchanged`** alias. +- T3. Plan patch expects **`019–194`**. diff --git a/docs/plans/2026-05-24-195-flat-unchanged-poll-numeric-plan.md b/docs/plans/2026-05-24-195-flat-unchanged-poll-numeric-plan.md new file mode 100644 index 000000000..bed831dd5 --- /dev/null +++ b/docs/plans/2026-05-24-195-flat-unchanged-poll-numeric-plan.md @@ -0,0 +1,28 @@ +--- +title: "feat: flat_unchanged=1 on gate-watch poll stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_unchanged=1 on Gate-Watch Poll Stderr (plan 195) + +## Summary + +Replace boolean **`flat_unchanged=true`** on compact gate-watch poll lines with numeric **`flat_unchanged=1`** to align with summary **`flat_unchanged=N`** tokens. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** emits **`flat_unchanged=1`** when flat keys unchanged (non-heartbeat poll). +- R2. Tests; **`PLAN_TRACK_CAP`** 195; closeout index **019–195**. + +--- + +## Test scenarios + +- T1. Unchanged poll → **`flat_unchanged=1`**, not **`flat_unchanged=true`**. +- T2. Changed keys → no **`flat_unchanged=`** token. +- T3. Plan patch expects **`019–195`**. diff --git a/docs/plans/2026-05-24-196-flat-unchanged-history-plan.md b/docs/plans/2026-05-24-196-flat-unchanged-history-plan.md new file mode 100644 index 000000000..a44636f14 --- /dev/null +++ b/docs/plans/2026-05-24-196-flat-unchanged-history-plan.md @@ -0,0 +1,28 @@ +--- +title: "feat: flat_unchanged streak in preflight watch history" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_unchanged Streak in Preflight Watch History (plan 196) + +## Summary + +Record **`flat_unchanged`** streak count on each **`preflight_watch_history`** snapshot when flat keys match the prior poll; record **`flat_hb=1`** on heartbeat polls. + +--- + +## Requirements + +- R1. Snapshots include **`flat_unchanged`** streak when **> 0**. +- R2. Snapshots include **`flat_hb: 1`** when a flat-keys heartbeat fires. +- R3. Tests; **`PLAN_TRACK_CAP`** 196; closeout index **019–196**. + +--- + +## Test scenarios + +- T1. Three unchanged deferred polls → history entries **`flat_unchanged`** 1 then 2. +- T2. Plan patch expects **`019–196`**. diff --git a/docs/plans/2026-05-24-197-max-flat-unchanged-summary-plan.md b/docs/plans/2026-05-24-197-max-flat-unchanged-summary-plan.md new file mode 100644 index 000000000..f2294621a --- /dev/null +++ b/docs/plans/2026-05-24-197-max-flat-unchanged-summary-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: max_flat_unchanged on preflight watch summary" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: max_flat_unchanged on Preflight Watch Summary (plan 197) + +## Summary + +Compute peak consecutive **`flat_unchanged`** streak from watch history and expose **`max_flat_unchanged`** on **`preflight_watch_summary`**. Emit summary stderr **`max_flat_unchanged=N`** when peak streak is below total unchanged polls (mid-watch key churn). + +--- + +## Requirements + +- R1. **`_max_preflight_flat_unchanged_streak`** scans history snapshot **`flat_unchanged`** values. +- R2. Summary JSON includes **`max_flat_unchanged`** when **> 0**. +- R3. Summary stderr emits **`max_flat_unchanged=N`** when **`N < flat_unchanged`** total. +- R4. Tests; **`PLAN_TRACK_CAP`** 197; closeout index **019–197**. + +--- + +## Test scenarios + +- T1. Continuous streak history → **`max_flat_unchanged=2`**, no extra stderr token. +- T2. Broken streak → **`max_flat_unchanged=1`**, total **2**, stderr includes **`max_flat_unchanged=1`**. +- T3. Plan patch expects **`019–197`**. diff --git a/docs/plans/2026-05-24-198-flat-unchanged-poll-streak-plan.md b/docs/plans/2026-05-24-198-flat-unchanged-poll-streak-plan.md new file mode 100644 index 000000000..820459aa2 --- /dev/null +++ b/docs/plans/2026-05-24-198-flat-unchanged-poll-streak-plan.md @@ -0,0 +1,28 @@ +--- +title: "feat: flat_unchanged streak on gate-watch poll stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_unchanged Streak on Gate-Watch Poll Stderr (plan 198) + +## Summary + +Emit **`flat_unchanged=N`** on compact gate-watch poll lines using the live unchanged streak count (replacing fixed **`flat_unchanged=1`**) to match history snapshots and summary tokens. + +--- + +## Requirements + +- R1. **`_format_preflight_watch_poll_line`** uses **`flat_keys_unchanged_streak`** for **`flat_unchanged=N`**. +- R2. Tests; **`PLAN_TRACK_CAP`** 198; closeout index **019–198**. + +--- + +## Test scenarios + +- T1. Streak 1 → **`flat_unchanged=1`**; streak 3 → **`flat_unchanged=3`**. +- T2. Heartbeat poll still uses **`flat_hb=1`**, not **`flat_unchanged=`**. +- T3. Plan patch expects **`019–198`**. diff --git a/docs/plans/2026-05-24-199-flat-hb-cumulative-plan.md b/docs/plans/2026-05-24-199-flat-hb-cumulative-plan.md new file mode 100644 index 000000000..09f4aff0b --- /dev/null +++ b/docs/plans/2026-05-24-199-flat-hb-cumulative-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: cumulative flat_hb on heartbeat poll stderr" +type: feat +status: active +date: 2026-05-28 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: Cumulative flat_hb on Heartbeat Poll Stderr (plan 199) + +## Summary + +Emit cumulative **`flat_hb=N`** on gate-watch heartbeat poll lines and add **`flat_hb_total`** JSON alias on **`preflight_watch_summary`**. History snapshots store cumulative **`flat_hb`** counts. + +--- + +## Requirements + +- R1. Heartbeat poll stderr uses cumulative heartbeat count (not fixed **`flat_hb=1`**). +- R2. Summary JSON includes **`flat_hb_total`** when heartbeats **> 0**. +- R3. History snapshots record cumulative **`flat_hb`** on heartbeat polls. +- R4. Tests; **`PLAN_TRACK_CAP`** 199; closeout index **019–199**. + +--- + +## Test scenarios + +- T1. Second heartbeat poll → **`flat_hb=2`** on stderr. +- T2. Summary JSON includes **`flat_hb_total`** alias. +- T3. Plan patch expects **`019–199`**. diff --git a/docs/plans/2026-05-24-200-flat-hb-total-summary-stderr-plan.md b/docs/plans/2026-05-24-200-flat-hb-total-summary-stderr-plan.md new file mode 100644 index 000000000..2a51820dd --- /dev/null +++ b/docs/plans/2026-05-24-200-flat-hb-total-summary-stderr-plan.md @@ -0,0 +1,30 @@ +--- +title: "feat: flat_hb_total token on preflight watch summary stderr" +type: feat +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_hb_total on Preflight Watch Summary Stderr (plan 200) + +## Summary + +Align preflight watch **summary stderr** with plan 199 JSON by emitting **`flat_hb_total=N`** instead of **`flat_hb=N`**. Poll lines keep compact **`flat_hb=N`**. + +--- + +## Requirements + +- R1. `_format_preflight_watch_summary_line` emits **`flat_hb_total=N`** when heartbeat summary gate passes. +- R2. Summary stderr omits legacy **`flat_hb=`** token. +- R3. Poll stderr unchanged (**`flat_hb=N`** cumulative). +- R4. Tests; **`PLAN_TRACK_CAP`** 200; closeout index **019–200**. + +--- + +## Test scenarios + +- T1. Summary line with heartbeats → **`flat_hb_total=1`** on stderr. +- T2. Early summary (unchanged < interval) omits **`flat_hb_total=`**. +- T3. Plan patch expects **`019–200`**. diff --git a/docs/plans/2026-05-24-201-flat-hb-total-history-plan.md b/docs/plans/2026-05-24-201-flat-hb-total-history-plan.md new file mode 100644 index 000000000..01c264d2d --- /dev/null +++ b/docs/plans/2026-05-24-201-flat-hb-total-history-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: flat_hb_total alias in preflight watch history snapshots" +type: feat +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_hb_total in Watch History Snapshots (plan 201) + +## Summary + +Add **`flat_hb_total`** to **`preflight_watch_history`** heartbeat snapshots alongside existing **`flat_hb`**, aligning history keys with summary JSON and stderr from plans 199–200. + +--- + +## Requirements + +- R1. Heartbeat poll snapshots record **`flat_hb_total`** (cumulative count). +- R2. Legacy **`flat_hb`** snapshot key retained for compatibility. +- R3. Tests; **`PLAN_TRACK_CAP`** 201; closeout index **019–201**. + +--- + +## Test scenarios + +- T1. Cumulative watch history entries include **`flat_hb_total=1`** then **`2`**. +- T2. Non-heartbeat snapshots omit **`flat_hb_total`**. +- T3. Plan patch expects **`019–201`**. diff --git a/docs/plans/2026-05-24-202-flat-hb-total-history-fallback-plan.md b/docs/plans/2026-05-24-202-flat-hb-total-history-fallback-plan.md new file mode 100644 index 000000000..9b561ef0d --- /dev/null +++ b/docs/plans/2026-05-24-202-flat-hb-total-history-fallback-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: derive flat_hb_total from watch history fallback" +type: feat +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_hb_total History Fallback in Watch Summary (plan 202) + +## Summary + +Add **`_max_preflight_flat_hb_total`** to read peak cumulative heartbeat count from **`preflight_watch_history`**, and use it as a fallback when building **`preflight_watch_summary`** if **`preflight_flat_keys_heartbeats`** is unset. + +--- + +## Requirements + +- R1. **`_max_preflight_flat_hb_total(history)`** prefers **`flat_hb_total`**, falls back to **`flat_hb`** per snapshot. +- R2. **`_build_preflight_watch_summary`** uses history fallback when status counter is zero. +- R3. Tests; **`PLAN_TRACK_CAP`** 202; closeout index **019–202**. + +--- + +## Test scenarios + +- T1. History-only status with heartbeat snapshots → summary **`flat_hb_total=2`**. +- T2. Status counter present still wins over history. +- T3. Plan patch expects **`019–202`**. diff --git a/docs/plans/2026-05-24-203-preflight-history-helpers-colocate-plan.md b/docs/plans/2026-05-24-203-preflight-history-helpers-colocate-plan.md new file mode 100644 index 000000000..1cb1227ca --- /dev/null +++ b/docs/plans/2026-05-24-203-preflight-history-helpers-colocate-plan.md @@ -0,0 +1,28 @@ +--- +title: "refactor: co-locate preflight watch history helpers" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Co-locate Preflight Watch History Helpers (plan 203) + +## Summary + +Move **`_count_unchanged_preflight_flat_keys_polls`**, **`_max_preflight_flat_unchanged_streak`**, and **`_max_preflight_flat_hb_total`** adjacent to **`_build_preflight_watch_summary`**, matching plans 186–187 helper clustering. + +--- + +## Requirements + +- R1. History helpers sit immediately above **`_build_preflight_watch_summary`**. +- R2. No behavior change. +- R3. Tests; **`PLAN_TRACK_CAP`** 203; closeout index **019–203**. + +--- + +## Test scenarios + +- T1. Existing history/count/max tests still pass. +- T2. Plan patch expects **`019–203`**. diff --git a/docs/plans/2026-05-24-204-preflight-poll-flat-stderr-parts-plan.md b/docs/plans/2026-05-24-204-preflight-poll-flat-stderr-parts-plan.md new file mode 100644 index 000000000..71d036842 --- /dev/null +++ b/docs/plans/2026-05-24-204-preflight-poll-flat-stderr-parts-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: extract preflight watch poll flat stderr parts" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Extract Preflight Watch Poll Flat Stderr Parts (plan 204) + +## Summary + +Extract **`_preflight_watch_poll_flat_stderr_parts`** from **`_format_preflight_watch_poll_line`**, co-locating unchanged/heartbeat flat-key stderr tokens with plan 190’s shared mirror helper pattern. + +--- + +## Requirements + +- R1. Poll line defers flat unchanged/heartbeat tokens to **`_preflight_watch_poll_flat_stderr_parts`**. +- R2. No stderr behavior change. +- R3. Tests; **`PLAN_TRACK_CAP`** 204; closeout index **019–204**. + +--- + +## Test scenarios + +- T1. Direct helper test for unchanged vs heartbeat branches. +- T2. Existing poll-line tests still pass. +- T3. Plan patch expects **`019–204`**. diff --git a/docs/plans/2026-05-24-205-preflight-summary-flat-stderr-parts-plan.md b/docs/plans/2026-05-24-205-preflight-summary-flat-stderr-parts-plan.md new file mode 100644 index 000000000..388bc15c8 --- /dev/null +++ b/docs/plans/2026-05-24-205-preflight-summary-flat-stderr-parts-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: extract preflight watch summary flat stderr parts" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Extract Preflight Watch Summary Flat Stderr Parts (plan 205) + +## Summary + +Extract **`_preflight_watch_summary_flat_stderr_parts`** from **`_format_preflight_watch_summary_line`**, pairing with plan 204’s poll-line flat stderr helper. + +--- + +## Requirements + +- R1. Summary line defers flat unchanged/heartbeat tokens to **`_preflight_watch_summary_flat_stderr_parts`**. +- R2. No stderr behavior change. +- R3. Tests; **`PLAN_TRACK_CAP`** 205; closeout index **019–205**. + +--- + +## Test scenarios + +- T1. Direct helper test for unchanged + heartbeat branches. +- T2. Existing summary-line tests still pass. +- T3. Plan patch expects **`019–205`**. diff --git a/docs/plans/2026-05-24-206-flat-unchanged-max-streak-fallback-plan.md b/docs/plans/2026-05-24-206-flat-unchanged-max-streak-fallback-plan.md new file mode 100644 index 000000000..7bb5ea6f3 --- /dev/null +++ b/docs/plans/2026-05-24-206-flat-unchanged-max-streak-fallback-plan.md @@ -0,0 +1,29 @@ +--- +title: "feat: flat_unchanged max-streak fallback in watch summary" +type: feat +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# feat: flat_unchanged Max-Streak Fallback in Watch Summary (plan 206) + +## Summary + +When **`unchanged_flat_keys_polls`** is zero, derive **`flat_unchanged`** in **`preflight_watch_summary`** from **`_max_preflight_flat_unchanged_streak`**, mirroring plan 202’s history fallback for **`flat_hb_total`**. + +--- + +## Requirements + +- R1. Summary uses max streak fallback before setting **`flat_unchanged`** alias. +- R2. **`unchanged_flat_keys_polls`** in summary reflects fallback value. +- R3. Tests; **`PLAN_TRACK_CAP`** 206; closeout index **019–206**. + +--- + +## Test scenarios + +- T1. History with streak snapshots only → **`flat_unchanged=2`**. +- T2. Pairwise count present still wins over fallback. +- T3. Plan patch expects **`019–206`**. diff --git a/docs/plans/2026-05-24-207-preflight-resolve-counters-plan.md b/docs/plans/2026-05-24-207-preflight-resolve-counters-plan.md new file mode 100644 index 000000000..2f0ad131b --- /dev/null +++ b/docs/plans/2026-05-24-207-preflight-resolve-counters-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: resolve preflight watch summary counters from history" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Resolve Preflight Watch Summary Counters (plan 207) + +## Summary + +Extract **`_resolve_preflight_flat_keys_heartbeats`** and **`_resolve_preflight_unchanged_flat_keys_polls`** to consolidate plan 202/206 history fallback logic used by **`_build_preflight_watch_summary`**. + +--- + +## Requirements + +- R1. Heartbeat resolve prefers status counter, then history max **`flat_hb_total`**. +- R2. Unchanged resolve prefers pairwise count, then max streak fallback. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 207; index **019–207**. + +--- + +## Test scenarios + +- T1. Resolve helpers unit tests for status/count preference and fallbacks. +- T2. Existing summary tests still pass. +- T3. Plan patch expects **`019–207`**. diff --git a/docs/plans/2026-05-24-208-preflight-flat-stderr-colocate-plan.md b/docs/plans/2026-05-24-208-preflight-flat-stderr-colocate-plan.md new file mode 100644 index 000000000..dde8903aa --- /dev/null +++ b/docs/plans/2026-05-24-208-preflight-flat-stderr-colocate-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: co-locate preflight flat stderr helpers and reuse interval resolver" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Co-locate Flat Stderr Helpers and Reuse Interval Resolver (plan 208) + +## Summary + +Co-locate **`_preflight_watch_poll_flat_stderr_parts`** with **`_preflight_watch_summary_flat_stderr_parts`**, and have the summary helper reuse **`_preflight_watch_heartbeat_interval`** for **`heartbeat_every=`** tokens. + +--- + +## Requirements + +- R1. Poll and summary flat stderr helpers are adjacent. +- R2. Summary flat stderr uses **`_preflight_watch_heartbeat_interval`** (no inline duplicate). +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 208; index **019–208**. + +--- + +## Test scenarios + +- T1. Summary flat stderr resolves **`heartbeat_every`** from **`watch_heartbeat_polls`** alias. +- T2. Existing poll/summary stderr tests pass. +- T3. Plan patch expects **`019–208`**. diff --git a/docs/plans/2026-05-24-209-preflight-max-flat-unchanged-resolver-plan.md b/docs/plans/2026-05-24-209-preflight-max-flat-unchanged-resolver-plan.md new file mode 100644 index 000000000..c137e7c94 --- /dev/null +++ b/docs/plans/2026-05-24-209-preflight-max-flat-unchanged-resolver-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: preflight max_flat_unchanged summary resolver" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Preflight max_flat_unchanged Summary Resolver (plan 209) + +## Summary + +Add **`_preflight_max_flat_unchanged`** resolver and use it in **`_preflight_watch_summary_flat_stderr_parts`** instead of inline **`summary.get("max_flat_unchanged")`**. + +--- + +## Requirements + +- R1. Resolver returns positive **`max_flat_unchanged`** from summary JSON. +- R2. Summary flat stderr uses resolver for gated **`max_flat_unchanged=`** token. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 209; index **019–209**. + +--- + +## Test scenarios + +- T1. Resolver returns peak streak from summary dict. +- T2. Summary stderr still emits **`max_flat_unchanged=1`** when peak < total. +- T3. Plan patch expects **`019–209`**. diff --git a/docs/plans/2026-05-24-210-preflight-max-flat-unchanged-stderr-plan.md b/docs/plans/2026-05-24-210-preflight-max-flat-unchanged-stderr-plan.md new file mode 100644 index 000000000..ebb3cddfd --- /dev/null +++ b/docs/plans/2026-05-24-210-preflight-max-flat-unchanged-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: preflight max_flat_unchanged stderr gate helper" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Preflight max_flat_unchanged Stderr Gate Helper (plan 210) + +## Summary + +Add **`_preflight_max_flat_unchanged_for_stderr`** to centralize gated **`max_flat_unchanged=`** emission in **`_preflight_watch_summary_flat_stderr_parts`**. + +--- + +## Requirements + +- R1. Helper returns peak streak only when **`0 < max < unchanged`**. +- R2. Summary flat stderr uses helper instead of inline gate. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 210; index **019–210**. + +--- + +## Test scenarios + +- T1. Helper returns **1** when peak **<** total unchanged. +- T2. Helper returns **0** when peak equals total unchanged. +- T3. Plan patch expects **`019–210`**. diff --git a/docs/plans/2026-05-24-211-preflight-flat-hb-total-stderr-plan.md b/docs/plans/2026-05-24-211-preflight-flat-hb-total-stderr-plan.md new file mode 100644 index 000000000..828390713 --- /dev/null +++ b/docs/plans/2026-05-24-211-preflight-flat-hb-total-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: preflight flat_hb_total stderr gate helper" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Preflight flat_hb_total Stderr Gate Helper (plan 211) + +## Summary + +Add **`_preflight_flat_hb_total_for_stderr`** to centralize gated **`flat_hb_total=`** emission in **`_preflight_watch_summary_flat_stderr_parts`**, pairing with plan 210’s max-unchanged gate helper. + +--- + +## Requirements + +- R1. Helper returns heartbeat count only when summary heartbeat gate passes. +- R2. Summary flat stderr uses helper instead of inline gate + count. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 211; index **019–211**. + +--- + +## Test scenarios + +- T1. Helper returns count when unchanged ≥ interval and heartbeats > 0. +- T2. Helper returns **0** when unchanged below interval. +- T3. Plan patch expects **`019–211`**. diff --git a/docs/plans/2026-05-24-212-preflight-heartbeat-every-stderr-plan.md b/docs/plans/2026-05-24-212-preflight-heartbeat-every-stderr-plan.md new file mode 100644 index 000000000..64b8f89fc --- /dev/null +++ b/docs/plans/2026-05-24-212-preflight-heartbeat-every-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: preflight heartbeat_every stderr gate helper" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Preflight heartbeat_every Stderr Gate Helper (plan 212) + +## Summary + +Add **`_preflight_heartbeat_every_for_stderr`** to emit **`heartbeat_every=`** on summary stderr only when unchanged flat-key polls occurred, reusing **`_preflight_watch_heartbeat_interval`**. + +--- + +## Requirements + +- R1. Helper returns interval only when **`_preflight_unchanged_flat_keys_polls` > 0**. +- R2. Summary flat stderr uses helper instead of inline unchanged guard + interval lookup. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 212; index **019–212**. + +--- + +## Test scenarios + +- T1. Helper returns **12** when unchanged polls occurred and interval set. +- T2. Helper returns **0** when no unchanged polls. +- T3. Plan patch expects **`019–212`**. diff --git a/docs/plans/2026-05-24-213-preflight-summary-unchanged-stderr-plan.md b/docs/plans/2026-05-24-213-preflight-summary-unchanged-stderr-plan.md new file mode 100644 index 000000000..943b16b0c --- /dev/null +++ b/docs/plans/2026-05-24-213-preflight-summary-unchanged-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: extract preflight summary unchanged flat stderr parts" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Extract Preflight Summary Unchanged Flat Stderr Parts (plan 213) + +## Summary + +Extract **`_preflight_watch_summary_unchanged_flat_stderr_parts`** from **`_preflight_watch_summary_flat_stderr_parts`**, composing plans 210–212 gate helpers for unchanged-block tokens. + +--- + +## Requirements + +- R1. Unchanged block helper emits **`flat_unchanged`**, gated **`max_flat_unchanged`**, gated **`heartbeat_every`**. +- R2. Summary flat stderr delegates unchanged block to helper, then appends **`flat_hb_total`**. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 213; index **019–213**. + +--- + +## Test scenarios + +- T1. Direct unchanged-block helper test with peak **<** total. +- T2. Existing summary flat stderr tests pass. +- T3. Plan patch expects **`019–213`**. diff --git a/docs/plans/2026-05-24-214-preflight-summary-heartbeat-stderr-plan.md b/docs/plans/2026-05-24-214-preflight-summary-heartbeat-stderr-plan.md new file mode 100644 index 000000000..ce55372a2 --- /dev/null +++ b/docs/plans/2026-05-24-214-preflight-summary-heartbeat-stderr-plan.md @@ -0,0 +1,29 @@ +--- +title: "refactor: extract preflight summary heartbeat flat stderr parts" +type: refactor +status: active +date: 2026-05-29 +origin: docs/solutions/testing/verify-pypi-regression-closeout.md +--- + +# refactor: Extract Preflight Summary Heartbeat Flat Stderr Parts (plan 214) + +## Summary + +Extract **`_preflight_watch_summary_heartbeat_flat_stderr_parts`** from **`_preflight_watch_summary_flat_stderr_parts`**, composing plan 211 gate helper for the heartbeat-block **`flat_hb_total`** token. + +--- + +## Requirements + +- R1. Heartbeat block helper emits gated **`flat_hb_total`** via **`_preflight_flat_hb_total_for_stderr`**. +- R2. Summary flat stderr composes unchanged block (plan 213) then heartbeat block. +- R3. No behavior change; tests; **`PLAN_TRACK_CAP`** 214; index **019–214**. + +--- + +## Test scenarios + +- T1. Direct heartbeat-block helper test when gate passes. +- T2. Existing summary flat stderr tests pass. +- T3. Plan patch expects **`019–214`**. diff --git a/docs/solutions/testing/verify-pypi-regression-closeout.md b/docs/solutions/testing/verify-pypi-regression-closeout.md index 651e81db3..60b412ae0 100644 --- a/docs/solutions/testing/verify-pypi-regression-closeout.md +++ b/docs/solutions/testing/verify-pypi-regression-closeout.md @@ -28,7 +28,7 @@ related_docs: | AGENTS.md (PyPI verify local parity) category: testing doc_status: current -last_verified: 2026-05-27 +last_verified: 2026-05-29 --- # Verify PyPI Regression Closeout @@ -81,8 +81,101 @@ Post–PR #268 CI hygiene and local parity for published PyPI packages. - **`doc_checkpoint_snapshot`** from solution doc when `gh_ok` false; blocked state **`gh_unavailable`** (plan 110). - Defer **`update_monitoring_docs`** until verify and FC are both terminal; **`fc_active_closeout_note`** (plan 111). - Defer briefing includes active **`fc_run_id`** / **`fc_run_url`** (and verify when active) (plan 112). -- Defer briefing **`monitor_commands`** — `watch_fc_run` / `watch_verify_run` + `preflight_retry` + `preflight_watch` (plans 113–114). -- **`--lfg-preflight-watch`** — poll preflight until defer clears or timeout (default 7200s); `preflight_watch_summary` (plan 114). +- Defer briefing **`monitor_commands`** — `watch_fc_run` / `watch_verify_run` + `preflight_retry` + `preflight_watch`; primary **`command`** uses preflight-watch when active; structured **`sha_gap`** when FC lags master (plans 113–117). +- Defer **`queue_context`** and **`primary_action: gate_watch`**; fc_active_pending sets **`queue_backlog_note`** when queued ≥ 4h (plan 120). +- Gate-watch poll stderr uses **`gate watch`** label; defer stderr **`queued=X.Xh`** from **`max_queued_hours`**; watch summary includes **`next_hint`** (plan 121). +- Defer briefing **`expected_after_terminal`** (prefetch_gate → gate → preflight); **`queue_backlog_warning`** at ≥2h with stderr **`queue_warn=true`**; watch summary **`reason=start->end`** (plan 122). +- **`investigate_ci_drift`** briefing adds **`expected_after_terminal`**, **`primary_action: gate_watch`**, and **`queue_context`** on wait paths; stderr **`expected_after=refresh_dry_run`** (plan 123). +- Defer briefing **`active_runs`** list and stderr **`active_runs=fc`**; closeout-style defer prefers **`expected_after_terminal.action=closeout`** (plan 124). +- Strict exit stderr and briefing stderr include **`verify_run=`**; exit line carries **`expected_after`** / **`active_runs`** from briefing (plan 125). +- Briefing stderr **`gh_watch=verify:ID,fc:ID`** when multiple active gh watches; watch summary JSON includes **`active_runs`** (plan 126). +- Briefing JSON **`gh_watch_summary`**; strict exit and watch summary one-liner stderr carry **`gh_watch=`** / **`active_runs=`** (plan 127). +- **`preflight_watch_summary`** JSON and one-liner stderr include **`gh_watch_summary`** / **`gh_watch=`** for active runs (plan 128). +- Top-level gate JSON **`gh_watch_summary`**; watch poll stderr **`gh_watch=`** (plan 129). +- Top-level gate JSON **`active_runs`**; strict exit stderr **`queued=`** / queue flags (plan 130). +- Top-level gate JSON **`queue_context`**; watch summary JSON/one-liner **`queued=`** (plan 131). +- Top-level gate JSON **`expected_after_terminal`** / **`primary_action`**; watch summary mirrors both (plan 132). +- Watch poll stderr aggregate **`queued=`**, queue flags, **`expected_after=`**, **`primary_action=`** (plan 133). +- Top-level gate JSON **`watch_recommended`**; strict exit and watch summary stderr (plan 134). +- Top-level gate JSON **`post_terminal_commands`**; watch summary JSON; poll **`watch_recommended=`** (plan 135). +- Top-level gate JSON **`wait_command`** and **`monitor_commands`**; watch summary mirrors both (plan 136). +- Top-level gate JSON **`verify_run_id`** / **`fc_run_id`**; watch summary mirrors both (plan 137). +- Top-level gate JSON **`verify_run_url`** / **`fc_run_url`**; watch summary mirrors both; strict exit stderr adds **`verify_run=`** / **`fc_run=`** (plan 138). +- Top-level gate JSON **`verify_status`** / **`fc_status`**; watch summary mirrors both; strict exit and summary one-liner add status words (plan 139). +- Top-level gate JSON **`blocked`**; watch summary mirrors it; strict exit and summary one-liner add **`blocked=`** (plan 140). +- Top-level gate JSON **`queue_backlog`** / **`queue_backlog_warning`** / **`queue_backlog_severe`** / **`max_queued_hours`** flattened from **`queue_context`**; watch summary mirrors all (plan 141). +- Top-level gate JSON **`briefing_action`**; watch summary mirrors it; strict exit and summary one-liner add **`action=`** (plan 142). +- Top-level gate JSON **`briefing_notes`** when checkpoint notes populate briefing; watch summary mirrors; strict exit and summary one-liner add **`notes=N`** (plan 143). +- Top-level gate JSON **`briefing_reason`**; watch summary mirrors it; strict exit and summary one-liner add **`briefing_reason=`** (plan 144). +- Top-level gate JSON **`briefing_merge_ready`**; watch summary mirrors it; strict exit and summary one-liner add **`merge_ready=`** (plan 145). +- Top-level gate JSON **`queue_backlog_note`** flattened from **`queue_context.note`**; watch summary mirrors; strict exit and summary one-liner add truncated **`queue_note=`** (plan 146). +- Top-level gate JSON **`sha_gap`** / **`sha_gap_short`** when FC SHA gap is active; watch summary mirrors; strict exit and summary one-liner add **`sha_gap=`** (plan 147). +- Top-level gate JSON **`gh_watch_command`**; watch summary mirrors it; strict exit and summary one-liner add **`watch=`** (plan 148). +- Top-level gate JSON **`briefing_command`** mirrors **`briefing.command`** (same as **`wait_command`**); watch poll stderr adds **`watch=`** / **`briefing_command=`**; strict exit and summary one-liner add truncated **`briefing_command=`** (plan 149). +- Deferred watch poll stderr adds truncated **`queue_note=`** from top-level **`queue_backlog_note`** (plan 150). +- Deferred watch poll stderr adds **`blocked=`** from top-level **`blocked`** (plan 151). +- Deferred watch poll stderr adds **`briefing_reason=`** from top-level **`briefing_reason`** (plan 152). +- Deferred watch poll stderr adds **`action=`** from top-level **`briefing_action`** (plan 153). +- Deferred watch poll stderr adds **`notes=N`** from top-level **`briefing_notes`** (plan 154). +- Deferred watch poll stderr adds **`merge_ready=`** from top-level **`briefing_merge_ready`** (plan 155). +- Deferred watch poll stderr adds **`verify_run=`** / **`fc_run=`** from top-level run IDs (plan 156). +- Deferred watch poll stderr adds **`verify_status=`** / **`fc_status=`** from top-level mirrored status (plan 157). +- Deferred watch poll stderr adds **`gh_watch=`** from top-level **`gh_watch_summary`** (plan 158). +- Deferred watch poll stderr adds **`queued=`** / queue flags from top-level flattened queue fields (plan 159). +- Deferred watch poll stderr adds **`active_runs=`** from top-level **`active_runs`** (plan 160). +- Deferred watch poll stderr adds truncated **`verify_run_url=`** / **`fc_run_url=`** from top-level run URLs (plan 161). +- Deferred watch poll stderr skips legacy **`verify=`** / **`fc=`** run IDs when **`verify_run=`** / **`fc_run=`** are mirrored (plan 162). +- Deferred watch poll stderr skips per-run **`verify_queued=`** / **`fc_queued=`** when top-level **`queued=`** is mirrored (plan 163). +- Deferred watch poll stderr emits **`sha_gap=`** from top-level **`sha_gap_short`** and skips pre-briefing checkpoint SHA gap (plan 164). +- Deferred watch poll stderr adds **`primary_action=`** / **`expected_after=`** from top-level mirrors (plan 165). +- Deferred watch poll stderr adds **`watch=`** / **`briefing_command=`** from top-level **`gh_watch_command`** / **`briefing_command`** (plan 166). +- Deferred watch poll stderr adds **`notes=N`** / **`merge_ready=`** from top-level **`briefing_notes`** / **`briefing_merge_ready`** (plan 167). +- Watch summary one-liner stderr adds **`verify_run=`** / **`fc_run=`** and truncated run URLs (plan 168). +- Watch summary one-liner stderr prefers top-level **`queued=`** / queue flags over nested **`queue_context`** (plan 169). +- **`preflight_watch_summary`** copies defer briefing mirrors from top-level **`status`** after **`_apply_lfg_agent_briefing`**, not nested **`lfg_agent_briefing`** (plan 170). +- Strict exit and deferred poll stderr share **`_lfg_briefing_mirror_stderr_parts`**, preferring top-level **`status`** with briefing fallback (plan 171). +- Watch summary one-liner stderr reuses **`_lfg_briefing_mirror_stderr_parts`** after watch prefix tokens (plan 172). +- **`LFG briefing:`** stderr reuses mirror parts from top-level **`status`** after apply; keeps **`reason=`** / **`drift_fields=`** / **`complete=`** (plan 173). +- Top-level gate JSON **`wait_recommended`** and **`ci_drift`** flattened from investigate-drift briefing; watch summary JSON mirrors both (plan 174). +- Shared mirror stderr emits **`wait=true`** and **`drift_fields=`** from top-level status; briefing emit reuses helper (plan 175). +- **`_mirror_lfg_flat_fields`** shared by apply and preflight watch summary JSON mirrors (plan 176). +- Gate JSON includes **`lfg_flat_field_keys`** legend listing top-level flattened briefing fields (plan 177). +- Gate JSON includes **`lfg_flat_field_values`** with only populated flattened fields for compact agent reads (plan 178). +- Shared mirror stderr includes **`flat_fields=N`** populated flat-field count for quick poll scans (plan 179). +- Strict-exit stderr attaches mirror tokens when top-level flat fields exist without nested **`lfg_agent_briefing`** (plan 180). +- Gate JSON includes **`lfg_flat_field_keys_present`** listing populated flat fields in canonical order (plan 181). +- Shared mirror stderr includes **`flat_keys=k1,k2,...`** from present-keys for poll diffs (plan 182). +- Gate-watch poll stderr omits **`flat_keys=`** / **`flat_fields=`** when present-keys unchanged; emits **`flat_unchanged=true`** (plan 183). +- **`preflight_watch_summary.unchanged_flat_keys_polls`** counts consecutive polls with identical **`flat_keys`** snapshots (plan 184). +- Gate-watch poll stderr re-emits full **`flat_keys=`** every **`--watch-heartbeat-polls`** unchanged flat-key polls (plan 185). +- **`_mirror_lfg_flat_fields`** lives with other **`_mirror_*`** briefing/queue helpers (plan 186). +- **`_mirror_preflight_watch_summary_from_status`** sits with flat-field mirror/build helpers (plan 187). +- Preflight watch summary stderr emits **`flat_keys_heartbeat_polls=`** only when unchanged flat-key polls reach **`watch_heartbeat_polls`** (plan 188). +- Preflight watch summary stderr includes **`watch_heartbeat_polls=`** when any unchanged flat-key polls occurred (plan 189; stderr token renamed **`heartbeat_every=`** in plan 191). +- Shared **`_lfg_flat_field_mirror_stderr_parts`** co-locates flat-field stderr tokens; unchanged poll lines emit **`heartbeat_every=N`** (plan 190). +- Preflight watch summary stderr uses **`heartbeat_every=N`** (same token as poll lines) when unchanged flat-key polls occurred (plan 191). +- **`preflight_watch_summary`** JSON includes **`heartbeat_every`** alias; gated summary stderr uses compact **`flat_hb=N`** (plan 192). +- Gate-watch heartbeat poll stderr uses cumulative **`flat_hb=N`**; summary JSON adds **`flat_hb_total`** alias; summary stderr uses **`flat_hb_total=N`** (plans 199–200). +- Preflight watch summary stderr uses compact **`flat_unchanged=N`**; JSON adds **`flat_unchanged`** alias (plan 194). +- Gate-watch poll stderr uses numeric **`flat_unchanged=N`** streak on unchanged polls (plan 198; was fixed **`=1`** in plan 195). +- **`preflight_watch_history`** snapshots record **`flat_unchanged`** streak and **`flat_hb`** / **`flat_hb_total`** on heartbeat polls (plans 196, 201). +- **`_resolve_preflight_flat_keys_heartbeats`** / **`_resolve_preflight_unchanged_flat_keys_polls`** consolidate history fallbacks for watch summary (plan 207). +- **`preflight_watch_summary`** derives **`flat_unchanged`** from history max streak when pairwise unchanged count is zero (plan 206). +- **`_preflight_watch_summary_heartbeat_flat_stderr_parts`** composes heartbeat-block summary stderr tokens (plan 214). +- **`_preflight_watch_summary_unchanged_flat_stderr_parts`** composes unchanged-block summary stderr tokens (plan 213). +- **`_preflight_heartbeat_every_for_stderr`** gates summary stderr **`heartbeat_every=`** when unchanged flat-key polls occurred (plan 212). +- **`_preflight_flat_hb_total_for_stderr`** gates summary stderr **`flat_hb_total=`** when heartbeat summary gate passes (plan 211). +- **`_preflight_max_flat_unchanged_for_stderr`** gates summary stderr **`max_flat_unchanged=`** when peak **<** total unchanged (plan 210). +- **`_preflight_max_flat_unchanged`** resolver reads peak unchanged streak from summary JSON (plan 209). +- **`_preflight_watch_poll_flat_stderr_parts`** sits with **`_preflight_watch_summary_flat_stderr_parts`**; summary helper reuses **`_preflight_watch_heartbeat_interval`** (plan 208). +- Shared **`_preflight_watch_summary_flat_stderr_parts`** co-locates watch summary unchanged/heartbeat tokens (plan 205). +- Shared **`_preflight_watch_poll_flat_stderr_parts`** co-locates gate-watch unchanged/heartbeat poll tokens (plan 204). +- Preflight watch history helpers (**`_count_unchanged_preflight_flat_keys_polls`**, **`_max_preflight_flat_unchanged_streak`**, **`_max_preflight_flat_hb_total`**) sit with **`_build_preflight_watch_summary`** (plan 203). +- **`preflight_watch_summary`** derives **`flat_hb_total`** from history when status heartbeat counter is unset (plan 202). +- **`preflight_watch_summary`** JSON includes peak **`max_flat_unchanged`** streak; stderr emits it when below total unchanged polls (plan 197). +- **`--lfg-preflight-watch`** — poll preflight until defer clears or timeout (default 7200s); `preflight_watch_summary` with `next_hint` (plan 114). +- **`--lfg-gate-watch`** — gate + preflight-watch; defer **`post_terminal_commands`** for after FC terminal; primary wait command for defer/drift (plans 118–119). +- **`investigate_ci_drift`** briefing includes structured **`drift`**, **`refresh_commands`**, and **`wait_recommended`** when runs are still active (plan 115). - **`pr_merged`** / **`pr_closed`** lifecycle blocked states (plan 091). - **`--lfg-closeout`** — same as **`--lfg-refresh --write`**; apply monitoring doc updates when CI is terminal (plan 080). - **`lfg_mode`** in JSON — `gate`, `merge_gate`, `pr_watch`, `preflight`, `refresh`, or `closeout` for agent routing (plans 080, 085). @@ -158,16 +251,16 @@ python3 .github/scripts/local_verify_pypi_slice.py --json | Workflow | Run | Notes | |----------|-----|-------| -| Verify PyPI | [26372746392](https://github.com/OpenKotOR/PyKotor/actions/runs/26372746392) | Check trigger success on `8916e2f`| -| Forward Commits | [26547345351](https://github.com/OpenKotOR/PyKotor/actions/runs/26547345351) | merge pending on `44ccf2a`| +| Verify PyPI | [26549547772](https://github.com/OpenKotOR/PyKotor/actions/runs/26549547772) | Check trigger success on `ca61ce8`| +| Forward Commits | [26549293445](https://github.com/OpenKotOR/PyKotor/actions/runs/26549293445) | merge failure on `ca61ce8`| ## Plans index -Plans **019–112** under `docs/plans/2026-05-24-*` document the closeout track; plan **020** is the authoritative verification table. +Plans **019–214** under `docs/plans/2026-05-24-*` document the closeout track; plan **020** is the authoritative verification table. -## Last CI check (plan 114) +## Last CI check (plan 214) -**2026-05-27:** verify [26372746392](https://github.com/OpenKotOR/PyKotor/actions/runs/26372746392) **success** on `8916e2f`; FC [26547345351](https://github.com/OpenKotOR/PyKotor/actions/runs/26547345351) **pending** on `44ccf2a`. +**2026-05-29:** verify [26549547772](https://github.com/OpenKotOR/PyKotor/actions/runs/26549547772) **success** on `ca61ce8`; FC [26549293445](https://github.com/OpenKotOR/PyKotor/actions/runs/26549293445) **failure** on `ca61ce8`. ## Track status (plan 106)