{"record":{"id":"d84c55412aab6b11","repo":"usestrix/strix","slug":"scan-start-is-unavailable","errorCode":null,"errorMessage":"Scan start is unavailable","messagePattern":"Scan start is unavailable","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"strix/interface/tui/backend/controller.py","lineNumber":315,"sourceCode":"    async def _start(self, payload: dict[str, Any]) -> dict[str, Any]:\n        if self.scan_started or self._start_in_progress:\n            raise RuntimeError(\"Scan is already starting or running\")\n        # A bare prompt launches optimistically, like a coding agent: it skips\n        # the network model preflight and surfaces any model error live. A named\n        # target keeps the preflight so a real scan does not commit blind.\n        verify = payload.get(\"verify\", True)\n        if not isinstance(verify, bool):\n            raise TypeError(\"verify must be a boolean\")\n        # Launching with no target mounts the working directory, so it requires\n        # the user's explicit confirmation rather than happening silently.\n        mount_working_dir = payload.get(\"mount_working_dir\", False)\n        if not isinstance(mount_working_dir, bool):\n            raise TypeError(\"mount_working_dir must be a boolean\")\n        model = (load_settings().llm.model or \"\").strip()\n        if not model:\n            raise ValueError(\"No model configured. Set STRIX_LLM first.\")\n        if self._on_start is None:\n            raise RuntimeError(\"Scan start is unavailable\")\n        if not self.targets:\n            if not mount_working_dir:\n                raise ValueError(\"No target set. Add a target first.\")\n            # Mounting the working directory needs the user's confirmation, and\n            # that is asked in the live view. Enter it now and prepare nothing\n            # until the answer arrives, so declining leaves no run behind.\n            self.pending_workspace_mount = str(Path.cwd())\n            self._pending_verify = verify\n            self.setup_mode = False\n            self.scan_started = True\n            self.scan_state = \"preparing\"\n            return {\"started\": True}\n        await self._begin_scan(verify)\n        return {\"started\": True}\n\n    async def _begin_scan(self, verify: bool) -> None:\n        if self._on_start is None:\n            raise RuntimeError(\"Scan start is unavailable\")","sourceCodeStart":297,"sourceCodeEnd":333,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L297-L333","documentation":"Raised by _start (strix/interface/tui/backend/controller.py:315) when the controller was constructed without an _on_start callback. The callback is the wiring that actually launches the scan; without it the TUI shell can collect setup input but cannot begin a run, so pressing start raises RuntimeError. This is an internal wiring/dependency-injection defect, not a user input error.","triggerScenarios":"Instantiating SetupController (or equivalent) directly in tests or embedding code without passing the on_start coroutine; a TUI composition refactor that forgot to inject the launcher; version skew where the constructor signature changed.","commonSituations":"Writing automated tests for the controller without a fake on_start; embedding the backend controller in a custom host app; partial upgrades where an old caller no longer supplies the callback.","solutions":["Pass an async on_start callback when constructing the controller (see how the production TUI runtime wires it).","In tests, inject a stub: controller = Controller(on_start=fake_start) before sending setup.start.","Check the constructor signature for your Strix version — the parameter may have been renamed/added."],"exampleFix":"# before\ncontroller = SetupController()              # no on_start\nawait controller.handle(\"setup.start\", {})   # RuntimeError: Scan start is unavailable\n\n# after\nasync def fake_start(verify: bool) -> None: ...\ncontroller = SetupController(on_start=fake_start)\nawait controller.handle(\"setup.start\", {})","handlingStrategy":"type-guard","validationCode":"if getattr(controller, '_on_start', None) is None:\n    raise RuntimeError('controller has no on_start; wire the launcher before use')","typeGuard":"def controller_can_start(controller) -> bool:\n    return getattr(controller, '_on_start', None) is not None","tryCatchPattern":"try:\n    await controller.handle(\"setup.start\", {})\nexcept RuntimeError as exc:\n    if 'Scan start is unavailable' in str(exc):\n        raise RuntimeError('embedding bug: construct controller with on_start=...') from exc\n    raise","preventionTips":["Always inject an async on_start when constructing the controller (production TUI does).","In tests, pass a stub launcher.","Assert the wiring in a smoke test for custom hosts."],"tags":["tui","dependency-injection","internal-error","wiring"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}