From 45c164060382959102f7c0b4044a5fe79147413b Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:46:17 +0300 Subject: [PATCH 1/8] Branch for FU-P7-T3-2: exclude broker-owned listener guidance From a89c260d7e5e5330e7224806ff2ef1abcbac37d1 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:46:37 +0300 Subject: [PATCH 2/8] Select task FU-P7-T3-2: Exclude broker-owned dashboard listeners from foreign port-conflict guidance --- SPECS/INPROGRESS/next.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index be61ab0..d6c8edf 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -4,7 +4,7 @@ **Phase:** Phase 7: Broker UX and Diagnostics **Effort:** 2-3 hours **Dependencies:** FU-P7-T3-1 -**Status:** Ready +**Status:** Selected ## Description From 0b3472ef8b4ebd4eda8328d5a091a092c0c40731 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:47:49 +0300 Subject: [PATCH 3/8] Plan task FU-P7-T3-2: Exclude broker-owned dashboard listeners from foreign port-conflict guidance --- ...ers_from_foreign_port-conflict_guidance.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md diff --git a/SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md b/SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md new file mode 100644 index 0000000..323f71d --- /dev/null +++ b/SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md @@ -0,0 +1,124 @@ +# FU-P7-T3-2 — Exclude broker-owned dashboard listeners from foreign port-conflict guidance + +## Objective Summary + +`FU-P7-T3-1` fixed the case where a live broker PID and a foreign process on +the requested dashboard port were collapsed into broker-only remediation. +Review of that change found one remaining ownership gap: the mixed-state branch +currently treats any listener on the dashboard port as foreign, even when the +listener belongs to the same broker daemon named by the PID file. When backend +probes degrade while the broker still owns the port, startup and `--doctor` +can incorrectly tell users to stop an "existing listener" or use +`--web-ui-restart`, even though the right guidance is still the broker-health +path. + +This follow-up should keep the mixed-state foreign-listener behavior from +`FU-P7-T3-1`, but make it ownership-aware. The implementation should filter out +the broker daemon PID before choosing the foreign-listener/port-occupied path, +while leaving single-blocker behavior unchanged for genuine foreign listeners +and broker-without-dashboard states. + +## Deliverables + +- Update `src/mcpbridge_wrapper/__main__.py` so mixed-state startup guidance + only treats listener PIDs that differ from the running broker PID as foreign + port conflicts. +- Update `src/mcpbridge_wrapper/doctor.py` so mixed-state diagnostics use the + same broker-owned-listener exclusion before returning `port-occupied`. +- Extend `tests/unit/test_main.py` and `tests/unit/test_doctor.py` with + broker-owned-listener regression coverage alongside the existing + foreign-listener tests. +- Produce `SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md` with targeted and + full quality-gate results. + +## Success Criteria + +- `--broker-console` and `--broker-daemon --web-ui` only surface + foreign-listener occupied-port guidance when `listener_pids` contains at + least one PID other than the running broker PID. +- `--doctor` does not classify a broker-owned dashboard listener plus degraded + probes as "stop the existing listener"; it stays on broker-health guidance. +- Regression tests cover both the foreign-listener and broker-owned-listener + mixed states so future refactors cannot reintroduce self-conflict messaging. + +## Test-First Plan + +1. Extend the existing startup tests for `_run_broker_console()` and + `main()`’s `--broker-daemon --web-ui` flow with a same-PID listener case, + asserting the error stays on the broker-reset/broker-health path and does + not mention "existing listener" or `--web-ui-restart`. +2. Extend the mixed-state doctor classification coverage with a dashboard + listener list that contains the local broker PID, asserting the report stays + in the `broker-without-dashboard` family instead of `port-occupied`. +3. Implement the smallest production change needed to derive a foreign-listener + subset before selecting user-facing remediation. +4. Run `pytest`, `ruff check src/`, `mypy src/`, and `pytest --cov`. + +## Execution Plan + +### Phase 1: Pin the self-listener contract + +Inputs: +- `tests/unit/test_main.py` +- `tests/unit/test_doctor.py` +- existing foreign-listener regressions from `FU-P7-T3-1` + +Outputs: +- failing tests for broker-owned listener mixed states in startup and doctor + +Verification: +- the new tests fail against the current branch because self-owned listeners + still route through foreign-listener guidance + +### Phase 2: Filter broker-owned listener PIDs + +Inputs: +- `_report_requested_dashboard_unavailable()` and `_run_broker_console()` in + `src/mcpbridge_wrapper/__main__.py` +- `classify_doctor_report()` in `src/mcpbridge_wrapper/doctor.py` + +Outputs: +- a minimal ownership-aware listener filter shared by startup and doctor logic +- unchanged messaging for genuine foreign listeners and plain + broker-without-dashboard states + +Verification: +- mixed-state occupied-port guidance only appears when a non-broker PID still + owns the dashboard port + +### Phase 3: Validate and archive + +Inputs: +- updated production code +- targeted and full test suites + +Outputs: +- passing targeted regressions and full quality gates +- validation report ready for ARCHIVE + +Verification: +- coverage remains at or above the repository threshold +- review can focus on ownership precision rather than broad behavior changes + +## Acceptance Tests + +- `pytest tests/unit/test_main.py` +- `pytest tests/unit/test_doctor.py` +- `pytest` +- `ruff check src/` +- `mypy src/` +- `pytest --cov` + +## Decision Points + +- Prefer filtering the listener PID set over rewriting the message hierarchy; + the user-visible behavior from `FU-P7-T3-1` should remain intact for genuine + foreign listeners. +- If multiple listener PIDs are present and one matches the broker PID, the + code should still report a port conflict when any remaining PID is foreign. + +## Notes + +- No documentation updates are expected unless stderr wording changes beyond + the existing broker-health and foreign-listener guidance surfaces. +- Review subject name for this task: `broker_owned_listener_guidance`. From 54452369413a8ef8c086734f5dd78ed1bc3edf4d Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:52:16 +0300 Subject: [PATCH 4/8] Implement FU-P7-T3-2: exclude broker-owned listener conflicts --- .../FU-P7-T3-2_Validation_Report.md | 99 +++++++++++++++++++ src/mcpbridge_wrapper/__main__.py | 15 ++- src/mcpbridge_wrapper/doctor.py | 12 ++- tests/unit/test_doctor.py | 23 ++++- tests/unit/test_main.py | 94 +++++++++++++++++- 5 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md diff --git a/SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md b/SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md new file mode 100644 index 0000000..9094d8c --- /dev/null +++ b/SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md @@ -0,0 +1,99 @@ +# FU-P7-T3-2 Validation Report + +**Task:** FU-P7-T3-2 — Exclude broker-owned dashboard listeners from foreign port-conflict guidance +**Date:** 2026-03-07 +**Verdict:** PASS + +## Summary + +Implemented the `FU-P7-T3-2` follow-up by: + +- updating `src/mcpbridge_wrapper/__main__.py` so mixed-state startup guidance + filters out the running broker PID before treating dashboard listeners as a + foreign port conflict +- updating `src/mcpbridge_wrapper/doctor.py` so doctor classification applies + the same ownership filter before returning `port-occupied` +- extending startup and doctor regressions so foreign-listener cases still + trigger occupied-port guidance, while broker-owned-listener cases stay on the + broker-health reset path + +## Files Validated + +- `src/mcpbridge_wrapper/__main__.py` +- `src/mcpbridge_wrapper/doctor.py` +- `tests/unit/test_main.py` +- `tests/unit/test_doctor.py` + +## Targeted Verification + +```bash +pytest tests/unit/test_main.py tests/unit/test_doctor.py -k 'same_pid_listener or broker_owned_listener or mixed_broker_and_foreign_listener_prefers_port_conflict or mixed_state_mentions_foreign_listener' +``` + +- Result: `6 passed` +- Observed outcome: foreign-listener mixed states still surface occupied-port + guidance, while same-PID listener states stay on broker-health guidance. + +```bash +pytest tests/unit/test_main.py tests/unit/test_doctor.py +``` + +- Result: `122 passed` + +## Required Quality Gates + +```bash +pytest +``` + +- Result: `894 passed, 5 skipped in 7.95s` + +```bash +ruff check src/ +``` + +- Result: `All checks passed!` + +```bash +mypy src/ +``` + +- Result: `Success: no issues found in 20 source files` + +```bash +pytest --cov +``` + +- Result: `894 passed, 5 skipped in 8.75s` +- Coverage: `91.78%` + +## Acceptance Criteria Evidence + +- [x] `--broker-console` and `--broker-daemon --web-ui` only surface + foreign-listener occupied-port guidance when `listener_pids` contains at + least one PID other than the running broker PID. + - Evidence: + `tests/unit/test_main.py::TestBrokerConsoleHelpers::test_run_broker_console_mixed_state_mentions_foreign_listener`, + `tests/unit/test_main.py::TestBrokerConsoleHelpers::test_run_broker_console_same_pid_listener_uses_broker_reset_guidance`, + `tests/unit/test_main.py::TestMainBrokerWebUIFlowCoverage::test_main_broker_daemon_webui_mixed_state_mentions_foreign_listener`, + and + `tests/unit/test_main.py::TestMainBrokerWebUIFlowCoverage::test_main_broker_daemon_webui_same_pid_listener_uses_broker_reset_guidance` + all passed. +- [x] `--doctor` does not classify a broker-owned dashboard listener plus + degraded probes as "stop the existing listener"; it stays on broker-health + guidance. + - Evidence: + `tests/unit/test_doctor.py::TestClassifyDoctorReport::test_classify_broker_owned_listener_uses_broker_without_dashboard` + passed with `report.code == "broker-without-dashboard"`, while + `test_classify_mixed_broker_and_foreign_listener_prefers_port_conflict` + still passed with `report.code == "port-occupied"`. +- [x] Regression tests cover both the foreign-listener and broker-owned-listener + mixed states so future refactors cannot reintroduce self-conflict messaging. + - Evidence: `tests/unit/test_main.py` and `tests/unit/test_doctor.py` now + cover mixed sets containing both the broker PID and a foreign PID, plus the + same-PID-only path. + +## Notes + +- Existing `websockets` / `uvicorn` deprecation warnings remain in the suite + and are unrelated to this task. diff --git a/src/mcpbridge_wrapper/__main__.py b/src/mcpbridge_wrapper/__main__.py index a61e229..d7c64e7 100644 --- a/src/mcpbridge_wrapper/__main__.py +++ b/src/mcpbridge_wrapper/__main__.py @@ -601,6 +601,15 @@ def _format_listener_pid_summary(listener_pids: Set[int]) -> str: return f"listener {label} {joined}" +def _foreign_listener_pids( + listener_pids: Set[int], running_broker_pid: Optional[int] +) -> Set[int]: + """Return listener PIDs that do not belong to the running broker.""" + if running_broker_pid is None: + return set(listener_pids) + return {pid for pid in listener_pids if pid != running_broker_pid} + + def _report_requested_dashboard_unavailable( *, runtime: Any, @@ -610,11 +619,13 @@ def _report_requested_dashboard_unavailable( listener_pids: Set[int], ) -> int: """Print one explicit remediation path for an unusable requested dashboard.""" - if port is not None and listener_pids and running_broker_pid is not None: + foreign_listener_pids = _foreign_listener_pids(listener_pids, running_broker_pid) + + if port is not None and foreign_listener_pids and running_broker_pid is not None: print( "Error: Broker daemon is already running " f"(PID {running_broker_pid}), but Web UI port {port} is already occupied by " - f"{_format_listener_pid_summary(listener_pids)}, so no broker-backed " + f"{_format_listener_pid_summary(foreign_listener_pids)}, so no broker-backed " f"dashboard is reachable at {runtime.base_url}. Stop the existing listener or " f"retry startup with {_broker_console_restart_command()}. If the port " f"becomes free and the dashboard is still unavailable, reset the dedicated " diff --git a/src/mcpbridge_wrapper/doctor.py b/src/mcpbridge_wrapper/doctor.py index d4b18c4..f385665 100644 --- a/src/mcpbridge_wrapper/doctor.py +++ b/src/mcpbridge_wrapper/doctor.py @@ -152,6 +152,13 @@ def _find_listener_pids_for_port(port: int | None) -> list[int]: return sorted(set(pids)) +def _foreign_listener_pids(listener_pids: list[int], local_pid: int | None) -> list[int]: + """Return listener PIDs that do not belong to the local broker PID.""" + if local_pid is None: + return list(listener_pids) + return [pid for pid in listener_pids if pid != local_pid] + + def _read_process_command(pid: int) -> str | None: """Return the command line for a PID when available.""" try: @@ -269,6 +276,7 @@ def classify_doctor_report( broker_runtime_lines: list[str] = [] evidence_lines: list[str] = [] + foreign_listener_pids = _foreign_listener_pids(dashboard.listener_pids, local.pid) if local.version_mismatch and local.pid_running: return DoctorReport( @@ -434,7 +442,7 @@ def classify_doctor_report( evidence_lines=evidence_lines, ) - if local.pid_running and dashboard.listener_pids: + if local.pid_running and foreign_listener_pids: evidence_lines.append( "A listener already owns the configured dashboard port while the local broker PID " "is still running." @@ -465,7 +473,7 @@ def classify_doctor_report( evidence_lines=evidence_lines, ) - if dashboard.listener_pids and (dashboard.health_ok or dashboard.backend_error): + if foreign_listener_pids and (dashboard.health_ok or dashboard.backend_error): evidence_lines.append( "A listener already owns the configured dashboard port, but it is " "not exposing broker-daemon." diff --git a/tests/unit/test_doctor.py b/tests/unit/test_doctor.py index f097868..433afd1 100644 --- a/tests/unit/test_doctor.py +++ b/tests/unit/test_doctor.py @@ -315,8 +315,11 @@ def test_classify_broker_running_without_dashboard(self) -> None: def test_classify_mixed_broker_and_foreign_listener_prefers_port_conflict(self) -> None: dashboard = _dashboard( - listener_pids=[777], - listener_commands={777: "node /tmp/other-service.js"}, + listener_pids=[100, 777], + listener_commands={ + 100: "python -m mcpbridge_wrapper --broker-daemon", + 777: "node /tmp/other-service.js", + }, health_ok=False, backend_error=None, ) @@ -330,6 +333,22 @@ def test_classify_mixed_broker_and_foreign_listener_prefers_port_conflict(self) assert any("Daemon PID: 100 (running)" in line for line in report.local_state_lines) assert any("777" in line for line in report.dashboard_lines) + def test_classify_broker_owned_listener_uses_broker_without_dashboard(self) -> None: + dashboard = _dashboard( + listener_pids=[100], + listener_commands={100: "python -m mcpbridge_wrapper --broker-daemon"}, + health_ok=False, + backend_error=None, + ) + + report = classify_doctor_report(_runtime(), _local(), dashboard) + + assert report.ok is False + assert report.code == "broker-without-dashboard" + assert "broker daemon is running" in report.summary.lower() + assert "--broker-stop && mcpbridge-wrapper --broker-console" in report.next_action + assert "--web-ui-restart" not in report.next_action + def test_classify_wrong_service_on_dashboard_port(self) -> None: dashboard = _dashboard( health_ok=True, diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 1ff2984..b9643e0 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1806,7 +1806,7 @@ def test_run_broker_console_mixed_state_mentions_foreign_listener(self, capsys): return_value=8080, ), patch( "mcpbridge_wrapper.__main__._find_listener_pids_for_port", - return_value={999}, + return_value={4242, 999}, ), patch("mcpbridge_wrapper.__main__._spawn_broker_console_host") as spawn_host: result = _run_broker_console( web_ui_port=8080, @@ -1819,9 +1819,45 @@ def test_run_broker_console_mixed_state_mentions_foreign_listener(self, capsys): assert "Broker daemon is already running (PID 4242)" in err assert "Web UI port 8080 is already occupied" in err assert "listener PID 999" in err + assert "listener PIDs 4242, 999" not in err assert "--web-ui-restart" in err spawn_host.assert_not_called() + def test_run_broker_console_same_pid_listener_uses_broker_reset_guidance(self, capsys): + from mcpbridge_wrapper.__main__ import _run_broker_console + + runtime = SimpleNamespace(base_url="http://127.0.0.1:8080", log_path="/tmp/broker.log") + with patch( + "mcpbridge_wrapper.tui.build_tui_runtime", + return_value=runtime, + ), patch( + "mcpbridge_wrapper.__main__._probe_broker_console_backend", + return_value=(False, "GET /api/broker/status failed: Not Found"), + ), patch( + "mcpbridge_wrapper.__main__._read_running_broker_pid", + return_value=4242, + ), patch( + "mcpbridge_wrapper.__main__._effective_web_ui_port", + return_value=8080, + ), patch( + "mcpbridge_wrapper.__main__._find_listener_pids_for_port", + return_value={4242}, + ), patch("mcpbridge_wrapper.__main__._spawn_broker_console_host") as spawn_host: + result = _run_broker_console( + web_ui_port=8080, + web_ui_config=None, + web_ui_restart=False, + ) + + assert result == 1 + err = capsys.readouterr().err + assert "Broker daemon is already running (PID 4242)" in err + assert "no broker-backed dashboard is reachable" in err + assert "--broker-stop" in err + assert "--web-ui-restart" not in err + assert "Stop the existing listener" not in err + spawn_host.assert_not_called() + def test_run_broker_console_rejects_foreign_dashboard_listener(self, capsys): from mcpbridge_wrapper.__main__ import _run_broker_console @@ -2311,7 +2347,7 @@ def test_main_broker_daemon_webui_mixed_state_mentions_foreign_listener(self, ca return_value=4242, ), patch( "mcpbridge_wrapper.__main__._find_listener_pids_for_port", - return_value={999}, + return_value={4242, 999}, ), patch("mcpbridge_wrapper.broker.types.BrokerConfig") as mock_cfg_cls, patch( "mcpbridge_wrapper.broker.daemon.BrokerDaemon" ) as mock_daemon_cls: @@ -2323,11 +2359,65 @@ def test_main_broker_daemon_webui_mixed_state_mentions_foreign_listener(self, ca assert "Broker daemon is already running (PID 4242)" in err assert "Web UI port 8080 is already occupied" in err assert "listener PID 999" in err + assert "listener PIDs 4242, 999" not in err assert "--web-ui-restart" in err run_server_in_thread.assert_not_called() mock_daemon_cls.assert_not_called() audit.close.assert_called_once_with() + def test_main_broker_daemon_webui_same_pid_listener_uses_broker_reset_guidance(self, capsys): + broker_cfg = MagicMock() + webui_config = MagicMock(spec=WebUIConfig) + webui_config.host = "127.0.0.1" + webui_config.port = 8080 + metrics = MagicMock() + audit = MagicMock() + is_port_available = MagicMock(return_value=False) + run_server = MagicMock() + run_server_in_thread = MagicMock() + + with patch( + "mcpbridge_wrapper.__main__.sys.argv", + ["mcpbridge-wrapper", "--broker-daemon", "--web-ui"], + ), patch( + "mcpbridge_wrapper.__main__._prepare_webui_runtime", + return_value=( + webui_config, + metrics, + audit, + is_port_available, + run_server, + run_server_in_thread, + ), + ), patch( + "mcpbridge_wrapper.tui.build_tui_runtime", + return_value=SimpleNamespace(base_url="http://127.0.0.1:8080"), + ), patch( + "mcpbridge_wrapper.__main__._probe_broker_console_backend", + return_value=(False, "GET /api/broker/status failed: Not Found"), + ), patch( + "mcpbridge_wrapper.__main__._read_running_broker_pid", + return_value=4242, + ), patch( + "mcpbridge_wrapper.__main__._find_listener_pids_for_port", + return_value={4242}, + ), patch("mcpbridge_wrapper.broker.types.BrokerConfig") as mock_cfg_cls, patch( + "mcpbridge_wrapper.broker.daemon.BrokerDaemon" + ) as mock_daemon_cls: + mock_cfg_cls.default.return_value = broker_cfg + result = main() + + assert result == 1 + err = capsys.readouterr().err + assert "Broker daemon is already running (PID 4242)" in err + assert "no broker-backed dashboard is reachable" in err + assert "--broker-stop" in err + assert "--web-ui-restart" not in err + assert "Stop the existing listener" not in err + run_server_in_thread.assert_not_called() + mock_daemon_cls.assert_not_called() + audit.close.assert_called_once_with() + def test_main_broker_daemon_webui_reuses_existing_backend_as_explicit_error(self, capsys): broker_cfg = MagicMock() webui_config = MagicMock(spec=WebUIConfig) From 8790132d80fa37ad21b6a591ee75693fc7282c5c Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:53:31 +0300 Subject: [PATCH 5/8] Archive task FU-P7-T3-2: Exclude broker-owned dashboard listeners from foreign port-conflict guidance (PASS) --- ...ers_from_foreign_port-conflict_guidance.md | 4 ++++ .../FU-P7-T3-2_Validation_Report.md | 0 SPECS/ARCHIVE/INDEX.md | 2 ++ SPECS/INPROGRESS/next.md | 24 +++++++++---------- SPECS/Workplan.md | 9 +++---- 5 files changed, 23 insertions(+), 16 deletions(-) rename SPECS/{INPROGRESS => ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance}/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md (99%) rename SPECS/{INPROGRESS => ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance}/FU-P7-T3-2_Validation_Report.md (100%) diff --git a/SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md b/SPECS/ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md similarity index 99% rename from SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md rename to SPECS/ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md index 323f71d..8ce8a76 100644 --- a/SPECS/INPROGRESS/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md +++ b/SPECS/ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance.md @@ -122,3 +122,7 @@ Verification: - No documentation updates are expected unless stderr wording changes beyond the existing broker-health and foreign-listener guidance surfaces. - Review subject name for this task: `broker_owned_listener_guidance`. + +--- +**Archived:** 2026-03-07 +**Verdict:** PASS diff --git a/SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md b/SPECS/ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/FU-P7-T3-2_Validation_Report.md similarity index 100% rename from SPECS/INPROGRESS/FU-P7-T3-2_Validation_Report.md rename to SPECS/ARCHIVE/FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/FU-P7-T3-2_Validation_Report.md diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index c7b59b0..0fc4859 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -6,6 +6,7 @@ | Task ID | Folder | Archived | Verdict | |---------|--------|----------|---------| +| FU-P7-T3-2 | [FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/](FU-P7-T3-2_Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance/) | 2026-03-07 | PASS | | FU-P7-T3-1 | [FU-P7-T3-1_Prioritize_foreign_port-owner_guidance_in_mixed_broker-dashboard_conflicts/](FU-P7-T3-1_Prioritize_foreign_port-owner_guidance_in_mixed_broker-dashboard_conflicts/) | 2026-03-07 | PASS | | FU-P7-T1-1 | [FU-P7-T1-1_Normalize_KeyboardInterrupt_handling_when_broker-console_reuses_an_existing_host/](FU-P7-T1-1_Normalize_KeyboardInterrupt_handling_when_broker-console_reuses_an_existing_host/) | 2026-03-07 | PASS | | P7-T3 | [P7-T3_Auto-recover_or_guide_on_dashboard_port_ownership_conflicts/](P7-T3_Auto-recover_or_guide_on_dashboard_port_ownership_conflicts/) | 2026-03-07 | PASS | @@ -345,6 +346,7 @@ | Date | Task ID | Action | |------|---------|--------| +| 2026-03-07 | FU-P7-T3-2 | Archived Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance (PASS) | | 2026-03-07 | FU-P7-T3-1 | Archived REVIEW_mixed_dashboard_conflict_guidance report | | 2026-03-07 | FU-P7-T3-1 | Archived Prioritize_foreign_port-owner_guidance_in_mixed_broker-dashboard_conflicts (PASS) | | 2026-03-07 | FU-P7-T1-1 | Archived REVIEW_broker_console_keyboardinterrupt_reuse report | diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index d6c8edf..2d56b27 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -1,26 +1,26 @@ -# Next Task: FU-P7-T3-2 — Exclude broker-owned dashboard listeners from foreign port-conflict guidance +# Next Task: P7-T4 — Add direct local-status fallback for TUI when dashboard API is unavailable **Priority:** P1 **Phase:** Phase 7: Broker UX and Diagnostics -**Effort:** 2-3 hours -**Dependencies:** FU-P7-T3-1 -**Status:** Selected +**Effort:** 4-5 hours +**Dependencies:** P6-T2 +**Status:** Ready ## Description -Refine the mixed broker/dashboard conflict classifier so it distinguishes the -broker daemon's own dashboard listener from a foreign process on the same port. -When degraded probes occur against a broker-owned listener, startup and -diagnostics should keep users on broker-health guidance instead of reporting a -foreign port owner. +Reduce TUI dependence on the Web UI API by letting it fall back to local broker +state when the dashboard endpoint is unavailable. The TUI should still provide +useful diagnostics from PID/socket/version files and any directly accessible +broker status sources, while clearly indicating that live dashboard-backed +controls are unavailable. ## Recently Archived +- `2026-03-07` — `FU-P7-T3-2` archived with verdict `PASS` - `2026-03-07` — `FU-P7-T3-1` archived with verdict `PASS` - `2026-03-07` — `FU-P7-T1-1` archived with verdict `PASS` -- `2026-03-07` — `P7-T3` archived with verdict `PASS` ## Next Step -Create the `FU-P7-T3-2` PRD in `SPECS/INPROGRESS/`, then implement and validate -the broker-owned-listener exclusion in startup and doctor mixed-state guidance. +Create the `P7-T4` PRD in `SPECS/INPROGRESS/`, then implement and validate the +local broker-status fallback path for TUI. diff --git a/SPECS/Workplan.md b/SPECS/Workplan.md index 8ab3e2e..d467c16 100644 --- a/SPECS/Workplan.md +++ b/SPECS/Workplan.md @@ -543,7 +543,8 @@ Add new tasks using the canonical template in [TASK_TEMPLATE.md](TASK_TEMPLATE.m - [x] `--doctor` does not hide the foreign listener behind a generic broker-without-dashboard diagnosis in the same mixed state - [x] Regression tests cover the mixed-state conflict and prevent reordering back to broker-only guidance -#### ⬜️ FU-P7-T3-2: Exclude broker-owned dashboard listeners from foreign port-conflict guidance +#### ✅ FU-P7-T3-2: Exclude broker-owned dashboard listeners from foreign port-conflict guidance +- **Status:** ✅ Completed (2026-03-07) - **Description:** Refine the mixed broker/dashboard conflict classifier so it distinguishes the broker daemon's own dashboard listener from a foreign process on the same port. When degraded probes occur against a broker-owned listener, startup and diagnostics should keep users on broker-health guidance instead of reporting a foreign port owner. - **Priority:** P1 - **Dependencies:** FU-P7-T3-1 @@ -553,9 +554,9 @@ Add new tasks using the canonical template in [TASK_TEMPLATE.md](TASK_TEMPLATE.m - `src/mcpbridge_wrapper/doctor.py` mixed-state diagnostic guidance with the same broker-owned listener exclusion - `tests/unit/test_main.py` and `tests/unit/test_doctor.py` regression coverage for foreign-listener and broker-owned-listener mixed states - **Acceptance Criteria:** - - [ ] Mixed-state foreign-port guidance triggers only when the dashboard listener PID differs from the running broker PID - - [ ] Broker-owned listeners with degraded dashboard probes do not tell users to stop an "existing listener" or use restart guidance meant for foreign ownership - - [ ] Regression tests cover both foreign-listener and broker-owned-listener mixed states in startup and doctor paths + - [x] Mixed-state foreign-port guidance triggers only when the dashboard listener PID differs from the running broker PID + - [x] Broker-owned listeners with degraded dashboard probes do not tell users to stop an "existing listener" or use restart guidance meant for foreign ownership + - [x] Regression tests cover both foreign-listener and broker-owned-listener mixed states in startup and doctor paths #### ⬜️ P7-T4: Add direct local-status fallback for TUI when dashboard API is unavailable - **Description:** Reduce TUI dependence on the Web UI API by letting it fall back to local broker state when the dashboard endpoint is unavailable. The TUI should still provide useful diagnostics from PID/socket/version files and any directly accessible broker status sources, while clearly indicating that live dashboard-backed controls are unavailable. From 4c76237ef2ac6dd04c4469306be5a1b1c3032fdc Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:54:05 +0300 Subject: [PATCH 6/8] Review FU-P7-T3-2: broker_owned_listener_guidance --- .../REVIEW_broker_owned_listener_guidance.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md diff --git a/SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md b/SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md new file mode 100644 index 0000000..4f5b346 --- /dev/null +++ b/SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md @@ -0,0 +1,39 @@ +## REVIEW REPORT — broker_owned_listener_guidance + +**Scope:** origin/main..HEAD +**Files:** 9 + +### Summary Verdict +- [x] Approve +- [ ] Approve with comments +- [ ] Request changes +- [ ] Block + +### Critical Issues + +- None. + +### Secondary Issues + +- None. + +### Architectural Notes + +- The change stays scoped to PID ownership classification instead of rewriting + the broader mixed-state guidance flow introduced in `FU-P7-T3-1`. +- Startup and doctor now share the same core rule: only listener PIDs that + differ from the broker PID should trigger foreign port-conflict guidance. + +### Tests + +- `pytest tests/unit/test_main.py tests/unit/test_doctor.py -k 'same_pid_listener or broker_owned_listener or mixed_broker_and_foreign_listener_prefers_port_conflict or mixed_state_mentions_foreign_listener'` passed (`6 passed`) +- `pytest tests/unit/test_main.py tests/unit/test_doctor.py` passed (`122 passed`) +- `pytest` passed (`894 passed, 5 skipped`) +- `ruff check src/` passed +- `mypy src/` passed +- `pytest --cov` passed with `91.78%` coverage + +### Next Steps + +- No follow-up tasks required from this review. +- Archive the review artifact and continue to PR creation. From 1422c8c812a803a6148b6103b50e66898ef136d9 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:54:28 +0300 Subject: [PATCH 7/8] Archive REVIEW_broker_owned_listener_guidance report --- SPECS/ARCHIVE/INDEX.md | 2 ++ .../_Historical}/REVIEW_broker_owned_listener_guidance.md | 0 2 files changed, 2 insertions(+) rename SPECS/{INPROGRESS => ARCHIVE/_Historical}/REVIEW_broker_owned_listener_guidance.md (100%) diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index 0fc4859..08ee806 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -202,6 +202,7 @@ | File | Description | |------|-------------| +| [REVIEW_broker_owned_listener_guidance.md](_Historical/REVIEW_broker_owned_listener_guidance.md) | Review report for FU-P7-T3-2 | | [REVIEW_mixed_dashboard_conflict_guidance.md](_Historical/REVIEW_mixed_dashboard_conflict_guidance.md) | Review report for FU-P7-T3-1 | | [REVIEW_broker_console_keyboardinterrupt_reuse.md](_Historical/REVIEW_broker_console_keyboardinterrupt_reuse.md) | Review report for FU-P7-T1-1 | | [REVIEW_dashboard_port_ownership_conflicts.md](_Historical/REVIEW_dashboard_port_ownership_conflicts.md) | Review report for P7-T3 | @@ -346,6 +347,7 @@ | Date | Task ID | Action | |------|---------|--------| +| 2026-03-07 | FU-P7-T3-2 | Archived REVIEW_broker_owned_listener_guidance report | | 2026-03-07 | FU-P7-T3-2 | Archived Exclude_broker-owned_dashboard_listeners_from_foreign_port-conflict_guidance (PASS) | | 2026-03-07 | FU-P7-T3-1 | Archived REVIEW_mixed_dashboard_conflict_guidance report | | 2026-03-07 | FU-P7-T3-1 | Archived Prioritize_foreign_port-owner_guidance_in_mixed_broker-dashboard_conflicts (PASS) | diff --git a/SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md b/SPECS/ARCHIVE/_Historical/REVIEW_broker_owned_listener_guidance.md similarity index 100% rename from SPECS/INPROGRESS/REVIEW_broker_owned_listener_guidance.md rename to SPECS/ARCHIVE/_Historical/REVIEW_broker_owned_listener_guidance.md From 8c583f7ed0aa38a24087542012483883e72283b3 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Sat, 7 Mar 2026 19:57:46 +0300 Subject: [PATCH 8/8] Format FU-P7-T3-2: satisfy ruff formatter --- src/mcpbridge_wrapper/__main__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mcpbridge_wrapper/__main__.py b/src/mcpbridge_wrapper/__main__.py index d7c64e7..4f01f0d 100644 --- a/src/mcpbridge_wrapper/__main__.py +++ b/src/mcpbridge_wrapper/__main__.py @@ -601,9 +601,7 @@ def _format_listener_pid_summary(listener_pids: Set[int]) -> str: return f"listener {label} {joined}" -def _foreign_listener_pids( - listener_pids: Set[int], running_broker_pid: Optional[int] -) -> Set[int]: +def _foreign_listener_pids(listener_pids: Set[int], running_broker_pid: Optional[int]) -> Set[int]: """Return listener PIDs that do not belong to the running broker.""" if running_broker_pid is None: return set(listener_pids)