From 7ee60edeb99175a4ba9e9118d8cb553b2c800ab2 Mon Sep 17 00:00:00 2001 From: Wesley Simplicio Date: Mon, 1 Jun 2026 18:46:59 -0300 Subject: [PATCH 1/2] test: add llama.cpp local default contract --- tests/python/test_local_models.py | 126 ++++++++++++------------ tests/python/test_providers_local.py | 72 +++++++++----- tests/python/test_providers_shellout.py | 12 +-- 3 files changed, 119 insertions(+), 91 deletions(-) diff --git a/tests/python/test_local_models.py b/tests/python/test_local_models.py index 01ccd6a..43c5484 100644 --- a/tests/python/test_local_models.py +++ b/tests/python/test_local_models.py @@ -1,4 +1,4 @@ -"""Tests for hardware detection + local_models tier → spec mapping + +"""Tests for hardware detection + local_models tier -> spec mapping + ensure_recommended safety gate.""" from __future__ import annotations @@ -6,7 +6,8 @@ from simplicio.hardware import HardwareProfile, pick_tier from simplicio.local_models import ( - DEFAULT_LOCAL_OLLAMA_ID, + DEFAULT_LOCAL_FILE, + DEFAULT_LOCAL_MODEL_ID, ModelSpec, RECOMMENDATIONS, RecommendationResult, @@ -54,8 +55,9 @@ def test_evaluate_picks_correct_spec_per_tier() -> None: apple_silicon=False, tier=tier) r = evaluate(prof) assert r.spec.tier == tier - assert r.spec.ollama_id == RECOMMENDATIONS[tier].ollama_id - assert r.spec.ollama_id == DEFAULT_LOCAL_OLLAMA_ID + assert r.spec.model_id == RECOMMENDATIONS[tier].model_id + assert r.spec.model_id == DEFAULT_LOCAL_MODEL_ID + assert r.spec.filename == DEFAULT_LOCAL_FILE def test_evaluate_refuses_to_run_oversized_model( @@ -63,13 +65,13 @@ def test_evaluate_refuses_to_run_oversized_model( ) -> None: """A 16 GB laptop should NEVER get can_run=True for the 17.5 GB MoE even if the tier mapping somehow points there — safety margin enforces.""" - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) monkeypatch.setitem( RECOMMENDATIONS, "gpu-large", - ModelSpec("gpu-large", "too-large:latest", 17.5, "Too Large"), + ModelSpec("gpu-large", "local-llama/default", "repo/too-large", "too-large.gguf", 17.5, "Too Large"), ) # Force a mismatch: profile says cpu-small (small) but we set tier to # gpu-large to simulate a bad override @@ -80,151 +82,151 @@ def test_evaluate_refuses_to_run_oversized_model( assert "usable" in r.reason and "required" in r.reason -def test_evaluate_marks_ollama_absent(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: False) +def test_evaluate_marks_gguf_absent(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) prof = _profile(ram=8, vram=0) r = evaluate(prof) - assert r.can_pull is False - assert "ollama not installed" in r.reason + assert r.can_download is True + assert "local GGUF not installed" in r.reason -def test_apple_silicon_profile_can_run_minicpm5_at_24gb( +def test_apple_silicon_profile_can_run_qwen_gguf_at_24gb( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) prof = _profile(ram=24, vram=24, apple=True) r = evaluate(prof) assert r.spec.tier == "gpu-large" assert r.can_run is True - # not yet installed → can_pull=true, but caller still needs --install - assert r.can_pull is True + # not yet installed -> can_download=true, but caller still needs --install + assert r.can_download is True # ---- ensure_recommended opt-in gate ---- # -def test_ensure_recommended_does_not_pull_without_opt_in( +def test_ensure_recommended_does_not_download_without_opt_in( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Hard rule from issue #32: never auto-pull without explicit user consent. - Missing model + can_pull=True must STILL not call pull() unless - auto_pull=True or SIMPLICIO_AUTO_PULL=1 is set.""" + """Hard rule from issue #32: never auto-download without explicit consent. + Missing model + can_download=True must STILL not call download() unless + auto_download=True or SIMPLICIO_AUTO_DOWNLOAD=1 is set.""" monkeypatch.delenv("SIMPLICIO_AUTO_PULL", raising=False) - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + monkeypatch.delenv("SIMPLICIO_AUTO_DOWNLOAD", raising=False) + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) monkeypatch.setitem( RECOMMENDATIONS, "gpu-large", - ModelSpec("gpu-large", "too-large:latest", 17.5, "Too Large"), + ModelSpec("gpu-large", "local-llama/default", "repo/weights", "weights.gguf", 1.6, "Qwen GGUF"), ) - pulled = {"called": False} + downloaded = {"called": False} - def fake_pull(_id, timeout=1800): - pulled["called"] = True + def fake_download(_spec): + downloaded["called"] = True return True, "" - monkeypatch.setattr("simplicio.local_models.pull", fake_pull) + monkeypatch.setattr("simplicio.local_models.download", fake_download) from simplicio.local_models import ensure_recommended prof = _profile(ram=32, vram=24, apple=False) # gpu-large - r = ensure_recommended(prof, auto_pull=False) - assert pulled["called"] is False + r = ensure_recommended(prof, auto_download=False) + assert downloaded["called"] is False assert r.installed is False assert "opt in" in r.reason -def test_ensure_recommended_pulls_with_explicit_opt_in( +def test_ensure_recommended_downloads_with_explicit_opt_in( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) - pulled = {"called": False, "id": ""} + downloaded = {"called": False, "file": ""} - def fake_pull(model_id, timeout=1800): - pulled["called"] = True - pulled["id"] = model_id - return True, "pulled ok" + def fake_download(spec): + downloaded["called"] = True + downloaded["file"] = spec.filename + return True, "downloaded ok" - monkeypatch.setattr("simplicio.local_models.pull", fake_pull) + monkeypatch.setattr("simplicio.local_models.download", fake_download) from simplicio.local_models import ensure_recommended prof = _profile(ram=64, vram=24, apple=False) # gpu-large - r = ensure_recommended(prof, auto_pull=True) - assert pulled["called"] is True - assert pulled["id"] == RECOMMENDATIONS["gpu-large"].ollama_id + r = ensure_recommended(prof, auto_download=True) + assert downloaded["called"] is True + assert downloaded["file"] == RECOMMENDATIONS["gpu-large"].filename assert r.installed is True -def test_ensure_recommended_pulls_via_env_var( +def test_ensure_recommended_downloads_via_env_var( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setenv("SIMPLICIO_AUTO_PULL", "1") - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + monkeypatch.setenv("SIMPLICIO_AUTO_DOWNLOAD", "1") + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) called = {"value": False} - def fake_pull(_id, timeout=1800): + def fake_download(_spec): called["value"] = True return True, "" - monkeypatch.setattr("simplicio.local_models.pull", fake_pull) + monkeypatch.setattr("simplicio.local_models.download", fake_download) from simplicio.local_models import ensure_recommended prof = _profile(ram=64, vram=24, apple=False) - ensure_recommended(prof, auto_pull=False) + ensure_recommended(prof, auto_download=False) assert called["value"] is True -def test_ensure_recommended_skips_pull_when_already_installed( +def test_ensure_recommended_skips_download_when_already_installed( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: True) - pulled = {"called": False} + downloaded = {"called": False} - def fake_pull(_id, timeout=1800): - pulled["called"] = True + def fake_download(_spec): + downloaded["called"] = True return True, "" - monkeypatch.setattr("simplicio.local_models.pull", fake_pull) + monkeypatch.setattr("simplicio.local_models.download", fake_download) from simplicio.local_models import ensure_recommended prof = _profile(ram=64, vram=24, apple=False) - r = ensure_recommended(prof, auto_pull=True) - assert pulled["called"] is False + r = ensure_recommended(prof, auto_download=True) + assert downloaded["called"] is False assert r.installed is True -def test_ensure_recommended_refuses_pull_when_undersized( +def test_ensure_recommended_refuses_download_when_undersized( monkeypatch: pytest.MonkeyPatch, ) -> None: - """The hard guarantee: NEVER pull a model that doesn't fit. Even with - auto_pull=True, if can_run is false we don't touch the disk.""" - monkeypatch.setattr("simplicio.local_models.ollama_present", lambda: True) + """The hard guarantee: NEVER download a model that doesn't fit. Even with + auto_download=True, if can_run is false we don't touch the disk.""" + monkeypatch.setattr("simplicio.local_models.model_file_present", lambda _s: False) monkeypatch.setattr( "simplicio.local_models.is_installed", lambda _id: False) monkeypatch.setitem( RECOMMENDATIONS, "gpu-large", - ModelSpec("gpu-large", "too-large:latest", 17.5, "Too Large"), + ModelSpec("gpu-large", "local-llama/default", "repo/too-large", "too-large.gguf", 17.5, "Too Large"), ) - pulled = {"called": False} + downloaded = {"called": False} - def fake_pull(_id, timeout=1800): - pulled["called"] = True + def fake_download(_spec): + downloaded["called"] = True return True, "" - monkeypatch.setattr("simplicio.local_models.pull", fake_pull) + monkeypatch.setattr("simplicio.local_models.download", fake_download) from simplicio.local_models import ensure_recommended @@ -233,7 +235,7 @@ def fake_pull(_id, timeout=1800): os_name="Linux", ram_gb=8, vram_gb=0, apple_silicon=False, tier="gpu-large", ) - r = ensure_recommended(prof, auto_pull=True) - assert pulled["called"] is False + r = ensure_recommended(prof, auto_download=True) + assert downloaded["called"] is False assert r.can_run is False assert r.installed is False diff --git a/tests/python/test_providers_local.py b/tests/python/test_providers_local.py index c94606c..4ff63d9 100644 --- a/tests/python/test_providers_local.py +++ b/tests/python/test_providers_local.py @@ -1,4 +1,4 @@ -"""Tests for Path 4: in-process local inference (llama-cpp-python). +"""Tests for the default in-process local inference path (llama-cpp-python). The llama-cpp-python and huggingface-hub libs are optional extras that are not installed in CI, so we test the routing/spec resolution directly and stub the @@ -45,7 +45,7 @@ def _clean(tmp_path, monkeypatch): # --------------------------------------------------------------------------- # -# _is_local +# _is_local / default routing # --------------------------------------------------------------------------- # @@ -54,9 +54,9 @@ def test_is_local_explicit_prefix(): assert providers._is_local("local-llama/repo::a.gguf", "http://x") is True -def test_empty_config_uses_ollama_default_not_in_process_local(): - assert providers._is_default_ollama(None, None) is True - assert providers._is_default_ollama("", "") is True +def test_empty_config_uses_in_process_local_default(): + assert providers._is_default_local(None, None) is True + assert providers._is_default_local("", "") is True assert providers._is_local(None, None) is False @@ -130,8 +130,8 @@ def test_provider_id_local(): def test_info_local_auto_default(): s = providers.info() - assert "openbmb/minicpm5:latest" in s - assert "provider=ollama" in s + assert "local-llama/default" in s + assert "provider=local-llama" in s assert "key=not-needed" in s assert providers.LOCAL_DEFAULT_FILE in s @@ -156,16 +156,22 @@ def test_resolve_local_path_missing_file_raises(): def test_resolve_local_path_existing_file(tmp_path): f = tmp_path / "m.gguf" - f.write_bytes(b"x") + f.write_bytes(b"GGUF") assert providers._resolve_local_path(None, None, str(f)) == str(f) def test_resolve_local_path_downloads_from_hf(monkeypatch): + from pathlib import Path + import tempfile + + downloaded_path = Path(tempfile.mkdtemp()) / "weights.gguf" + downloaded_path.write_bytes(b"GGUF") + downloaded = str(downloaded_path) fake = types.ModuleType("huggingface_hub") - fake.hf_hub_download = MagicMock(return_value="/cache/weights.gguf") + fake.hf_hub_download = MagicMock(return_value=downloaded) monkeypatch.setitem(sys.modules, "huggingface_hub", fake) out = providers._resolve_local_path("owner/repo", "weights.gguf", None) - assert out == "/cache/weights.gguf" + assert out == downloaded fake.hf_hub_download.assert_called_once_with( repo_id="owner/repo", filename="weights.gguf" ) @@ -175,7 +181,7 @@ def test_resolve_local_path_prefers_executor_dir(monkeypatch, tmp_path): model_dir = tmp_path / "models" model_dir.mkdir() primary = model_dir / providers.LOCAL_DEFAULT_FILE - primary.write_bytes(b"x") + primary.write_bytes(b"GGUF") fake = types.ModuleType("huggingface_hub") fake.hf_hub_download = MagicMock() monkeypatch.setitem(sys.modules, "huggingface_hub", fake) @@ -189,6 +195,27 @@ def test_resolve_local_path_prefers_executor_dir(monkeypatch, tmp_path): fake.hf_hub_download.assert_not_called() +def test_resolve_local_path_skips_corrupt_executor_file(monkeypatch, tmp_path): + model_dir = tmp_path / "models" + model_dir.mkdir() + corrupt = model_dir / providers.LOCAL_DEFAULT_FILE + corrupt.write_bytes(b"not-a-gguf") + downloaded = tmp_path / "cache" / providers.LOCAL_DEFAULT_FILE + downloaded.parent.mkdir() + downloaded.write_bytes(b"GGUF") + fake = types.ModuleType("huggingface_hub") + fake.hf_hub_download = MagicMock(return_value=str(downloaded)) + monkeypatch.setitem(sys.modules, "huggingface_hub", fake) + monkeypatch.setenv("SIMPLICIO_LOCAL_MODEL_DIR", str(model_dir)) + + out = providers._resolve_local_path( + providers.LOCAL_DEFAULT_REPO, providers.LOCAL_DEFAULT_FILE, None + ) + + assert out == str(downloaded) + fake.hf_hub_download.assert_called_once() + + def test_resolve_local_path_default_does_not_try_legacy_qwen25(monkeypatch, tmp_path): fake = types.ModuleType("huggingface_hub") fake.hf_hub_download = MagicMock(side_effect=RuntimeError("qwen35 missing")) @@ -273,31 +300,31 @@ def test_local_llama_honours_ctx_threads_gpu(monkeypatch): # --------------------------------------------------------------------------- # -def test_generate_routes_to_ollama_by_default(monkeypatch): +def test_generate_routes_to_local_llama_by_default(monkeypatch): calls = [] - def fake_openai(model, base, key, prompt, feedback, max_tokens): + def fake_openai(*_args, **_kwargs): + raise AssertionError("Ollama/OpenAI-compatible endpoint must not be used") + + def fake_local(prompt, feedback, model, max_tokens): calls.append((prompt, model, max_tokens)) - return "OLLAMA OK" + return "GGUF OK" monkeypatch.setattr(providers, "_openai_compatible_generate", fake_openai) + monkeypatch.setattr(providers, "_local_generate", fake_local) out = providers.generate("do x", max_tokens=128) - assert out == "OLLAMA OK" - assert calls[0][1] == providers.DEFAULT_OLLAMA_MODEL + assert out == "GGUF OK" + assert calls[0][1] == "local-llama/default" assert calls[0][2] == 128 -def test_generate_default_ollama_falls_back_to_qwen_gguf(monkeypatch): +def test_generate_default_local_uses_qwen_gguf(monkeypatch): calls = [] - def fail_openai(*_args, **_kwargs): - raise RuntimeError("ollama down") - def fake_local(prompt, feedback, model, max_tokens): calls.append((prompt, model, max_tokens)) return "GGUF OK" - monkeypatch.setattr(providers, "_openai_compatible_generate", fail_openai) monkeypatch.setattr(providers, "_local_generate", fake_local) out = providers.generate("do x", max_tokens=128) assert out == "GGUF OK" @@ -384,5 +411,6 @@ def _touch(monkeypatch): fd, path = tempfile.mkstemp(suffix=".gguf") import os as _os - _os.close(fd) + with _os.fdopen(fd, "wb") as handle: + handle.write(b"GGUF") return path diff --git a/tests/python/test_providers_shellout.py b/tests/python/test_providers_shellout.py index c297d63..ec1b94c 100644 --- a/tests/python/test_providers_shellout.py +++ b/tests/python/test_providers_shellout.py @@ -182,15 +182,13 @@ def test_no_model_with_base_raises_with_hint(monkeypatch): assert "local-llama" in msg -def test_no_config_at_all_routes_to_ollama_default(monkeypatch): - # No model AND no base -> local Ollama primary (Path 4), not a raise. +def test_no_config_at_all_routes_to_local_llama_default(monkeypatch): + # No model AND no base -> local llama.cpp primary, not a raise. monkeypatch.delenv("SIMPLICIO_MODEL", raising=False) monkeypatch.delenv("SIMPLICIO_BASE_URL", raising=False) monkeypatch.setattr( providers, - "_openai_compatible_generate", - lambda model, base, key, p, f, mt: f"ollama:{model}@{base}", - ) - assert providers.generate("x") == ( - "ollama:openbmb/minicpm5:latest@http://localhost:11434/v1" + "_local_generate", + lambda p, f, model, mt: f"local:{model}", ) + assert providers.generate("x") == "local:local-llama/default" From 51249bbb10fb4d6277a8bc00921c0907448c7b76 Mon Sep 17 00:00:00 2001 From: Wesley Simplicio Date: Mon, 1 Jun 2026 18:59:37 -0300 Subject: [PATCH 2/2] fix: use llama.cpp as local default --- .simplicio/architecture-inventory.json | 68 +-- .simplicio/call-graph.json | 6 +- .simplicio/index-state.json | 12 +- .simplicio/precedent-index.json | 82 ++-- .simplicio/project-map.json | 393 ++++++++-------- .simplicio/symbol-index.json | 592 ++++++++++++++----------- CHANGELOG.md | 10 + README.md | 21 +- README.pt-BR.md | 21 +- docs/LLM_USAGE_POLICY.md | 48 +- docs/PYTHON_PACKAGE_INTERDEPENDENCE.md | 8 +- docs/agent-architecture.md | 2 +- pyproject.toml | 2 +- simplicio/__init__.py | 2 +- simplicio/cli.py | 30 +- simplicio/doctor.py | 63 ++- simplicio/local_models.py | 260 ++++++----- simplicio/providers.py | 125 ++---- tests/python/test_local_models.py | 1 - tests/python/test_package_metadata.py | 2 +- tests/python/test_run_cli.py | 16 + 21 files changed, 951 insertions(+), 813 deletions(-) diff --git a/.simplicio/architecture-inventory.json b/.simplicio/architecture-inventory.json index b58536b..dabd756 100644 --- a/.simplicio/architecture-inventory.json +++ b/.simplicio/architecture-inventory.json @@ -1,7 +1,7 @@ { "schema": "simplicio.architecture-inventory/v1", "version": 1, - "generated_at": "2026-06-01T05:04:23.593Z", + "generated_at": "2026-06-01T21:57:57.150Z", "root": "/Users/wesleysimplicio/Projetos/ai/simplicio-dev-cli", "source_project_map": ".simplicio/project-map.json", "source_symbol_index": ".simplicio/symbol-index.json", @@ -8730,6 +8730,7 @@ "._cache", ".bench", ".detect", + ".doctor", ".dod", ".ecosystem", ".init", @@ -8745,7 +8746,6 @@ ".sprint_loader", "__future__", "argparse", - "json", "pathlib" ], "symbols": [ @@ -9020,20 +9020,24 @@ ], "imports": [ ".hardware", + ".providers", "__future__", "dataclasses", + "huggingface_hub", "os", - "shutil", - "subprocess", - "typing" + "pathlib" ], "symbols": [ "ModelSpec", - "ollama_present", - "ollama_list_installed", + "ollama_id", + "local_model_dir", + "model_file_path", + "_is_gguf_file", + "model_file_present", "is_installed", - "pull", + "download", "RecommendationResult", + "can_pull", "to_dict", "evaluate", "ensure_recommended" @@ -9374,9 +9378,10 @@ "_msgs", "_inline_feedback", "_is_local", - "_is_default_ollama", + "_is_default_local", "_local_spec", "_local_executor_dir", + "_is_gguf_file", "_local_candidates", "_resolve_local_path", "_local_llama", @@ -9389,7 +9394,6 @@ "_charge_if_budgeted", "_generate_local_cached", "_openai_compatible_generate", - "_generate_default_ollama_with_fallback", "generate", "info", "planner_cfg", @@ -9398,7 +9402,7 @@ "planner_complete", "planner_info" ], - "summary": "Defines exported symbols: _cfg, _charge_if_budgeted, _cli_command, _generate_default_ollama_with_fallback, _generate_local_cached.", + "summary": "Defines exported symbols: _cfg, _charge_if_budgeted, _cli_command, _generate_local_cached, _inline_feedback.", "evidence": [ { "file": "simplicio/providers.py" @@ -15556,7 +15560,7 @@ "roles": [], "imports": [], "symbols": [], - "summary": "Participates in the project implementation; inspect imports and symbols for exact usage.", + "summary": "Defines exported symbols: do.", "evidence": [ { "file": "simplicio_cli.egg-info/PKG-INFO" @@ -16079,18 +16083,18 @@ "_profile", "test_evaluate_picks_correct_spec_per_tier", "test_evaluate_refuses_to_run_oversized_model", - "test_evaluate_marks_ollama_absent", - "test_apple_silicon_profile_can_run_minicpm5_at_24gb", - "test_ensure_recommended_does_not_pull_without_opt_in", - "fake_pull", - "test_ensure_recommended_pulls_with_explicit_opt_in", - "fake_pull", - "test_ensure_recommended_pulls_via_env_var", - "fake_pull", - "test_ensure_recommended_skips_pull_when_already_installed", - "fake_pull", - "test_ensure_recommended_refuses_pull_when_undersized", - "fake_pull" + "test_evaluate_marks_gguf_absent", + "test_apple_silicon_profile_can_run_qwen_gguf_at_24gb", + "test_ensure_recommended_does_not_download_without_opt_in", + "fake_download", + "test_ensure_recommended_downloads_with_explicit_opt_in", + "fake_download", + "test_ensure_recommended_downloads_via_env_var", + "fake_download", + "test_ensure_recommended_skips_download_when_already_installed", + "fake_download", + "test_ensure_recommended_refuses_download_when_undersized", + "fake_download" ], "summary": "Defines domain, data or schema structures.", "evidence": [ @@ -16314,6 +16318,7 @@ ], "imports": [ "os", + "pathlib", "pytest", "simplicio", "simplicio._cache", @@ -16325,7 +16330,7 @@ "symbols": [ "_clean", "test_is_local_explicit_prefix", - "test_empty_config_uses_ollama_default_not_in_process_local", + "test_empty_config_uses_in_process_local_default", "test_is_local_false_when_base_set", "test_is_local_false_when_other_model_set", "test_local_spec_default", @@ -16342,15 +16347,16 @@ "test_resolve_local_path_existing_file", "test_resolve_local_path_downloads_from_hf", "test_resolve_local_path_prefers_executor_dir", + "test_resolve_local_path_skips_corrupt_executor_file", "test_resolve_local_path_default_does_not_try_legacy_qwen25", "test_resolve_local_path_no_hf_lib_raises", "test_local_llama_missing_backend_raises", "test_local_llama_loads_and_caches", "test_local_llama_honours_ctx_threads_gpu", - "test_generate_routes_to_ollama_by_default", + "test_generate_routes_to_local_llama_by_default", "fake_openai", - "test_generate_default_ollama_falls_back_to_qwen_gguf", - "fail_openai", + "fake_local", + "test_generate_default_local_uses_qwen_gguf", "fake_local", "test_generate_local_explicit_prefix", "fake_local", @@ -16399,7 +16405,7 @@ "test_info_reports_shell_out_modes", "test_native_path_still_requires_key", "test_no_model_with_base_raises_with_hint", - "test_no_config_at_all_routes_to_ollama_default" + "test_no_config_at_all_routes_to_local_llama_default" ], "summary": "Verifies project behavior through automated tests.", "evidence": [ @@ -16507,6 +16513,8 @@ "test_index_accepts_positional_root", "fake_index_repo", "test_env_export_quotes_dotenv_values", + "test_doctor_command_delegates_to_local_model_preflight", + "fake_doctor_main", "test_run_auto_task_infers_target_from_goal", "test_run_ambiguous_goal_requires_scope", "test_run_scope_scratch_forwards_to_scratch_cli", @@ -20029,7 +20037,7 @@ "files": 686, "modules": 16, "layers": 13, - "symbols": 1805, + "symbols": 1812, "relationships": 1000, "tests": 99 }, diff --git a/.simplicio/call-graph.json b/.simplicio/call-graph.json index 0d83f9c..bef4a56 100644 --- a/.simplicio/call-graph.json +++ b/.simplicio/call-graph.json @@ -1,7 +1,7 @@ { "schema": "simplicio.call-graph/v1", "version": 1, - "generated_at": "2026-06-01T05:04:23.593Z", + "generated_at": "2026-06-01T21:57:57.150Z", "source_symbol_index": ".simplicio/symbol-index.json", "edges": [ { @@ -8950,8 +8950,8 @@ } ], "counts": { - "edges": 3730, + "edges": 3735, "imports": 150, - "calls": 3580 + "calls": 3585 } } diff --git a/.simplicio/index-state.json b/.simplicio/index-state.json index d30e8a8..865775c 100644 --- a/.simplicio/index-state.json +++ b/.simplicio/index-state.json @@ -1,19 +1,19 @@ { "counts": { - "changed_files": 23, + "changed_files": 19, "files": 686, "layers": 13, "modules": 16, "precedents": 250, "relationships": 1000, - "symbols": 1805 + "symbols": 1812 }, "schema": "simplicio.mapper-index-state/v1", "signature": { - "head": "03e995a483669e3470b6be9553aebc180321bea7", + "head": "7ee60edeb99175a4ba9e9118d8cb553b2c800ab2", "kind": "git", - "status_hash": "21323a8fa7a8f72eeb1c0e416419fe0be8b9495abfd5fe8cfc2d0b8616a17a45", - "tree_hash": "f66ce9936129b2e2f8b53342897e2080a66c1a4cb35d5872eca8237522a5636e" + "status_hash": "0beca92f92572d1cf59dea27a6b3492884cc5699ea01bd08853a079a9be7a861", + "tree_hash": "5bac91a905825c3f716b476fcdb7957755795e5cc92af5392d08a6e0f18dfc6c" }, - "updated_at": "2026-06-01T05:04:26Z" + "updated_at": "2026-06-01T21:57:58Z" } diff --git a/.simplicio/precedent-index.json b/.simplicio/precedent-index.json index 02d22fc..2eb7402 100644 --- a/.simplicio/precedent-index.json +++ b/.simplicio/precedent-index.json @@ -1,7 +1,7 @@ { "schema": "simplicio.precedent-index/v1", "version": 1, - "generated_at": "2026-06-01T05:04:23.593Z", + "generated_at": "2026-06-01T21:57:57.150Z", "source_project_map": ".simplicio/project-map.json", "items": [ { @@ -218,9 +218,9 @@ "snippet": "```typescript\n// tests/unit/calculate-invoice-total.test.ts\ntest('aplica desconto quando cliente e VIP', () => {\n const invoice = makeInvoice({ items: [{ price: 100 }], customer: { tier: 'vip' } });\n expect(calculateInvoiceTotal(invoice)).toBe(90);" }, { - "id": "166956a1bd091c6b", + "id": "4d1f5ffc82d15266", "path": "CHANGELOG.md", - "line": 254, + "line": 264, "language": "markdown", "change_type": "error-handling", "tags": [ @@ -258,6 +258,19 @@ "summary": "route precedent in README.md", "snippet": "\n> Reproduce the new default set:\n> `BENCH_BASE_URL=https://router.huggingface.co/v1 BENCH_API_KEY=\n> BENCH_MODELS=\"Qwen/Qwen3-Coder-30B-A3B-Instruct,Qwen/Qwen3-Coder-Next\"\n> python3 bench/run_offline.py`." }, + { + "id": "d3c9cccb8b4a7333", + "path": "README.pt-BR.md", + "line": 219, + "language": "markdown", + "change_type": "route", + "tags": [ + "markdown", + "readme" + ], + "summary": "route precedent in README.pt-BR.md", + "snippet": "\n> Reproduce the new default set:\n> `BENCH_BASE_URL=https://router.huggingface.co/v1 BENCH_API_KEY=\n> BENCH_MODELS=\"Qwen/Qwen3-Coder-30B-A3B-Instruct,Qwen/Qwen3-Coder-Next\"\n> python3 bench/run_offline.py`." + }, { "id": "98533d8a1a7fd947", "path": "bench/CONSOLIDATED_REPORT.md", @@ -1657,9 +1670,9 @@ "snippet": "\n@dataclass\nclass DetectResult:\n is_code_task: bool\n score: int" }, { - "id": "7352f3281a9ad12f", + "id": "f607717260587f91", "path": "simplicio/doctor.py", - "line": 23, + "line": 21, "language": "python", "change_type": "feature", "tags": [ @@ -1741,9 +1754,9 @@ "snippet": "\n@dataclass\nclass IntentResult:\n scope: str\n confidence: float" }, { - "id": "5ac7393621c8b503", + "id": "3552d415c23299fd", "path": "simplicio/local_models.py", - "line": 29, + "line": 40, "language": "python", "change_type": "feature", "tags": [ @@ -1754,7 +1767,7 @@ "models" ], "summary": "feature precedent in simplicio/local_models.py", - "snippet": "\n@dataclass\nclass ModelSpec:\n tier: str\n ollama_id: str" + "snippet": "\n@dataclass\nclass ModelSpec:\n tier: str\n model_id: str" }, { "id": "a8b82053a7fe99ad", @@ -1873,9 +1886,9 @@ "snippet": "# below stays the source of truth and is what pip-installed users get\n# until a wheel ships.\ntry:\n from simplicio_core import build_6layer_prompt as _rs_build\nexcept ImportError: # pragma: no cover - exercised at import time" }, { - "id": "3b6a48872b2c4fe6", + "id": "20890634d0753b48", "path": "simplicio/providers.py", - "line": 48, + "line": 46, "language": "python", "change_type": "feature", "tags": [ @@ -3683,6 +3696,23 @@ "summary": "error-handling precedent in simplicio/utils/serialization.py", "snippet": "from typing import Any\n\ntry: # optional fast path\n import orjson as _oj\n _HAS_ORJSON = True" }, + { + "id": "c7c06634143f64b7", + "path": "simplicio_cli.egg-info/PKG-INFO", + "line": 264, + "language": "text", + "change_type": "route", + "tags": [ + "text", + "simplicio", + "cli", + "egg", + "info", + "pkg" + ], + "summary": "route precedent in simplicio_cli.egg-info/PKG-INFO", + "snippet": "\n> Reproduce the new default set:\n> `BENCH_BASE_URL=https://router.huggingface.co/v1 BENCH_API_KEY=\n> BENCH_MODELS=\"Qwen/Qwen3-Coder-30B-A3B-Instruct,Qwen/Qwen3-Coder-Next\"\n> python3 bench/run_offline.py`." + }, { "id": "2cb77abf86cd4d7c", "path": "simplicio_cli.egg-info/SOURCES.txt", @@ -4206,38 +4236,6 @@ ], "summary": "test precedent in tests/python/test_scratch_codegen_fastapi.py", "snippet": "\n\ndef _stack(tmp_path: Path) -> Stack:\n return Stack(\n slug=\"py-fastapi\"," - }, - { - "id": "f5a96ce9e7bd69fe", - "path": "tests/python/test_scratch_codegen_go_gin.py", - "line": 31, - "language": "python", - "change_type": "test", - "tags": [ - "test", - "python", - "scratch", - "codegen", - "gin" - ], - "summary": "test precedent in tests/python/test_scratch_codegen_go_gin.py", - "snippet": "\n\ndef _go_binary() -> str | None:\n return shutil.which(\"go\") or (str(_PORTABLE_GO) if _PORTABLE_GO.is_file() else None)\n" - }, - { - "id": "65779c7c369949cd", - "path": "tests/python/test_scratch_codegen_markdown.py", - "line": 13, - "language": "python", - "change_type": "test", - "tags": [ - "test", - "python", - "scratch", - "codegen", - "markdown" - ], - "summary": "test precedent in tests/python/test_scratch_codegen_markdown.py", - "snippet": "\n\ndef _stack(tmp_path: Path) -> Stack:\n return Stack(\n slug=\"php-vanilla\"," } ] } diff --git a/.simplicio/project-map.json b/.simplicio/project-map.json index 744971b..b1c74e5 100644 --- a/.simplicio/project-map.json +++ b/.simplicio/project-map.json @@ -1,7 +1,7 @@ { "schema": "simplicio.project-map/v1", "version": 1, - "generated_at": "2026-06-01T05:04:23.593Z", + "generated_at": "2026-06-01T21:57:57.150Z", "update_mode": "incremental", "product": { "name": "simplicio-cli-e2e", @@ -924,9 +924,9 @@ { "path": "CHANGELOG.md", "language": "markdown", - "size_bytes": 36309, - "last_modified": "2026-06-01T05:04:04.189Z", - "file_hash": "e89e02ac0f8e6a004ad6c6c17d6c9724cdbfe09f3d0f63c1fe0b540a13807dec", + "size_bytes": 36728, + "last_modified": "2026-06-01T21:50:25.296Z", + "file_hash": "db105082c29d4dfad6fc1f9519b8b6e3a8460dfc01dfca0d7854429dc60d199c", "git_status": "M", "roles": [], "imports": [], @@ -1001,9 +1001,9 @@ { "path": "README.md", "language": "markdown", - "size_bytes": 43834, - "last_modified": "2026-06-01T05:02:57.151Z", - "file_hash": "f5411234898dc2f32887805976a7f475c9d120fd39f34459ee2ef59fa156c164", + "size_bytes": 43686, + "last_modified": "2026-06-01T21:50:07.179Z", + "file_hash": "6a9e75acd7fea857d40fe68a864eed36c0d8833676fe4ba8442059321a797ffa", "git_status": "M", "roles": [], "imports": [], @@ -1015,14 +1015,16 @@ { "path": "README.pt-BR.md", "language": "markdown", - "size_bytes": 23553, - "last_modified": "2026-06-01T05:02:57.161Z", - "file_hash": "7903d145f6760367d847c544cb955907f9eee89c67a669c7cfa896306d6a41db", + "size_bytes": 43790, + "last_modified": "2026-06-01T21:50:07.180Z", + "file_hash": "d3f3f575cd317d72cb97f826193aa20468de3211eaae232897cdd155bfb3924b", "git_status": "M", "roles": [], "imports": [], - "exports": [], - "importance": 0.32 + "exports": [ + "do" + ], + "importance": 0.4 }, { "path": "READMEs/README.ar-SA.md", @@ -1030,11 +1032,11 @@ "size_bytes": 5547, "last_modified": "2026-06-01T05:02:02.214Z", "file_hash": "1132cb60f970a67121294872d0d81be560b08d02ef6baa4336f168188bea5205", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.en.md", @@ -1042,11 +1044,11 @@ "size_bytes": 5192, "last_modified": "2026-06-01T05:02:02.214Z", "file_hash": "1eb978c94152eae81d347ed619c22dcb32b1f3a281013e6d431ef492fb5af23e", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.es-ES.md", @@ -1054,11 +1056,11 @@ "size_bytes": 5256, "last_modified": "2026-06-01T05:02:02.215Z", "file_hash": "81b2a7518952a74d04a039e0a8bc442213429369badc39eac49476d385337851", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.fr-FR.md", @@ -1066,11 +1068,11 @@ "size_bytes": 5308, "last_modified": "2026-06-01T05:02:02.215Z", "file_hash": "653764134c51f666edbe0d52a420331f1c9127f21a8fe16c3cea2a9c7718daec", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.he-IL.md", @@ -1078,11 +1080,11 @@ "size_bytes": 5513, "last_modified": "2026-06-01T05:02:02.215Z", "file_hash": "bb8add8515b6dda2a48c30392d4bd9341757919857f56b33317bdd3bc98f771f", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.hi-IN.md", @@ -1090,11 +1092,11 @@ "size_bytes": 5804, "last_modified": "2026-06-01T05:02:02.215Z", "file_hash": "55e9f9ef6e2fc9f6b500e087ed5be933be8c5627cb01299d3d56d1d278c9508d", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.id-ID.md", @@ -1102,11 +1104,11 @@ "size_bytes": 5233, "last_modified": "2026-06-01T05:02:02.216Z", "file_hash": "3be725ff44d566dce34aa4ef000f5bb2fcdac68c1f752d7b063f64f9fe70c53e", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.it-IT.md", @@ -1114,11 +1116,11 @@ "size_bytes": 5257, "last_modified": "2026-06-01T05:02:02.216Z", "file_hash": "f64ddb53fbf52eefcd7b512746738b11a3a520029cca3e8d1c39c770bd65bcd2", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.ja-JP.md", @@ -1126,11 +1128,11 @@ "size_bytes": 5367, "last_modified": "2026-06-01T05:02:02.217Z", "file_hash": "9c7d10735cefc8a6c0bc2eb987fa1dabc750d90624c67fb0364385750b7f1432", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.ko-KR.md", @@ -1138,11 +1140,11 @@ "size_bytes": 5316, "last_modified": "2026-06-01T05:02:02.217Z", "file_hash": "51a390b877767a6d3fc4a79c06f9e195f149bbcb65cf00fce4b42c73337cbdcb", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.ms-MY.md", @@ -1150,11 +1152,11 @@ "size_bytes": 5243, "last_modified": "2026-06-01T05:02:02.217Z", "file_hash": "ed49afd791610009a5eb88f861983d5498018a2c3a0368a58edd0e20e1c9ae79", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.pl-PL.md", @@ -1162,11 +1164,11 @@ "size_bytes": 5209, "last_modified": "2026-06-01T05:02:02.218Z", "file_hash": "fde58e040ee8baa11b97b62a5eb3a46f05f24b85e9d0f14165005f164dd1f978", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.pt-BR.md", @@ -1174,11 +1176,11 @@ "size_bytes": 5298, "last_modified": "2026-06-01T05:02:02.218Z", "file_hash": "e120cf25f307542e318d324b4911de7ecd654ef55319959894c6379c271a6945", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.ru-RU.md", @@ -1186,11 +1188,11 @@ "size_bytes": 5897, "last_modified": "2026-06-01T05:02:02.219Z", "file_hash": "6994cde9ebc332d7fbd4915ef94b27ace2ba114078f32effbf2f1f4d8599484e", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "READMEs/README.zh-CN.md", @@ -1198,11 +1200,11 @@ "size_bytes": 5113, "last_modified": "2026-06-01T05:02:02.219Z", "file_hash": "123b56c7550b5d6f4654e09ab5e43c226a9ae5950f301e9ca73ee01af127a3e3", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "RELATORIO_CONSOLIDADO_E_RECOMENDACAO.md", @@ -4105,21 +4107,21 @@ { "path": "docs/LLM_USAGE_POLICY.md", "language": "markdown", - "size_bytes": 3130, - "last_modified": "2026-06-01T03:02:46.242Z", - "file_hash": "3ec02b411c440e8b4614d660afcfaef79a805622b4be6a7aaa01b0ef84b7f673", - "git_status": "clean", + "size_bytes": 3807, + "last_modified": "2026-06-01T21:55:37.280Z", + "file_hash": "fae3be5cc66d98aa7c11eb19a5dfd596e18cb9f7ea5e4f3739c4edd96339a778", + "git_status": "M", "roles": [], "imports": [], "exports": [], - "importance": 0.12 + "importance": 0.32 }, { "path": "docs/PYTHON_PACKAGE_INTERDEPENDENCE.md", "language": "markdown", - "size_bytes": 794, - "last_modified": "2026-06-01T05:04:04.189Z", - "file_hash": "ee704d8425e5759e8e81e837b96b14d24c39241a210c27dc72bbbebab2c6601f", + "size_bytes": 935, + "last_modified": "2026-06-01T21:55:37.281Z", + "file_hash": "a507d8b3496c3893cd7b1da9b4c928acb41b1e067a63e5336a8afff296cc9c81", "git_status": "M", "roles": [], "imports": [], @@ -4142,13 +4144,13 @@ "path": "docs/agent-architecture.md", "language": "markdown", "size_bytes": 16207, - "last_modified": "2026-05-27T19:05:30.505Z", - "file_hash": "7d1be1c853d8e48dd3ea9fd86c8282a88054db764ff07c6aaf9188d0b40ac51f", - "git_status": "clean", + "last_modified": "2026-06-01T21:55:37.281Z", + "file_hash": "81acb7bfbddd21c43a497445af6678880d48fe72eba519e5a015dbc2eb653313", + "git_status": "M", "roles": [], "imports": [], "exports": [], - "importance": 0.12 + "importance": 0.32 }, { "path": "docs/api-examples/README.md", @@ -4328,11 +4330,11 @@ "size_bytes": 3743, "last_modified": "2026-06-01T05:02:02.219Z", "file_hash": "7bf8b3b7011bc6ffe2ad96f2a7d437cb5c0d1522946aea4632d08c00cf64bcdf", - "git_status": "M", + "git_status": "clean", "roles": [], "imports": [], "exports": [], - "importance": 0.32 + "importance": 0.12 }, { "path": "docs/sessionstart-hook.md", @@ -4436,8 +4438,8 @@ "path": "pyproject.toml", "language": "toml", "size_bytes": 2676, - "last_modified": "2026-06-01T05:04:04.188Z", - "file_hash": "4ca37b8ae8510733c2d3094ad4cea3208a4fa1f3c6761650257140837b5668ed", + "last_modified": "2026-06-01T21:50:25.292Z", + "file_hash": "d04126b14f1488a1777b0940f814e4366b3718bd57facb2c3050cbf351e5a260", "git_status": "M", "roles": [ "config" @@ -4695,8 +4697,8 @@ "path": "simplicio/__init__.py", "language": "python", "size_bytes": 23, - "last_modified": "2026-06-01T05:04:04.188Z", - "file_hash": "9f9412338be7514766825b066af8c02e683fd0a347ec17c7607532cf61e8bb3b", + "last_modified": "2026-06-01T21:50:25.294Z", + "file_hash": "7936300cd2f328749b8c21d55aba13dcedf682ea554078ba632731f5073d4e18", "git_status": "M", "roles": [], "imports": [], @@ -4843,10 +4845,10 @@ { "path": "simplicio/cli.py", "language": "python", - "size_bytes": 24593, - "last_modified": "2026-06-01T03:01:23.428Z", - "file_hash": "9cc1a24515c6861b83341f057d2556a55ca5f3053f8a81fa087e2419a83ecb68", - "git_status": "clean", + "size_bytes": 25114, + "last_modified": "2026-06-01T21:51:11.240Z", + "file_hash": "c904371d43ecfb9bff6c7cffd4b54a9d4148e74eff34e0c54da39a453537b512", + "git_status": "M", "roles": [ "entrypoint" ], @@ -4854,6 +4856,7 @@ "._cache", ".bench", ".detect", + ".doctor", ".dod", ".ecosystem", ".init", @@ -4869,7 +4872,6 @@ ".sprint_loader", "__future__", "argparse", - "json", "pathlib" ], "exports": [ @@ -4891,7 +4893,7 @@ "main", "maybe_autoinstall" ], - "importance": 0.73 + "importance": 0.93 }, { "path": "simplicio/detect.py", @@ -4922,10 +4924,10 @@ { "path": "simplicio/doctor.py", "language": "python", - "size_bytes": 4133, - "last_modified": "2026-05-31T10:10:13.527Z", - "file_hash": "336b6ced22a1897ccfd6950006600d0c2d53dcc031c8e4e299153a6f2301bacf", - "git_status": "clean", + "size_bytes": 4008, + "last_modified": "2026-06-01T21:49:29.267Z", + "file_hash": "3542b5d8a354820b42fd584eb6969df81d6cae405321ff6b678d7bd8a5ceba59", + "git_status": "M", "roles": [], "imports": [ ".hardware", @@ -4939,7 +4941,7 @@ "_render_human", "main" ], - "importance": 0.28 + "importance": 0.48 }, { "path": "simplicio/dod.py", @@ -5095,34 +5097,38 @@ { "path": "simplicio/local_models.py", "language": "python", - "size_bytes": 6985, - "last_modified": "2026-06-01T03:08:48.790Z", - "file_hash": "5c46866d57b2605061bbca2687b7dbedb3ad2fada517b09c751d0385b5ea0e21", - "git_status": "clean", + "size_bytes": 7432, + "last_modified": "2026-06-01T21:49:10.276Z", + "file_hash": "2c397a3f886eeabd4d5abb53a13a0bd36d432187a111b126a6c15acb9ceba585", + "git_status": "M", "roles": [ "domain" ], "imports": [ ".hardware", + ".providers", "__future__", "dataclasses", + "huggingface_hub", "os", - "shutil", - "subprocess", - "typing" + "pathlib" ], "exports": [ "ModelSpec", "RecommendationResult", + "_is_gguf_file", + "can_pull", + "download", "ensure_recommended", "evaluate", "is_installed", - "ollama_list_installed", - "ollama_present", - "pull", + "local_model_dir", + "model_file_path", + "model_file_present", + "ollama_id", "to_dict" ], - "importance": 0.48 + "importance": 0.68 }, { "path": "simplicio/mapper.py", @@ -5384,10 +5390,10 @@ { "path": "simplicio/providers.py", "language": "python", - "size_bytes": 26202, - "last_modified": "2026-06-01T03:06:33.643Z", - "file_hash": "92c7d1f0829c1ec3691758902a2316558d3d3bed4b7b1b27a6f00f4f66c3fa7d", - "git_status": "clean", + "size_bytes": 25339, + "last_modified": "2026-06-01T21:54:37.612Z", + "file_hash": "f58073673ede4da67705072f09b30bda79905b2468a1e1dd2861517b7cdbe564", + "git_status": "M", "roles": [], "imports": [ "._cache", @@ -5405,10 +5411,10 @@ "_cfg", "_charge_if_budgeted", "_cli_command", - "_generate_default_ollama_with_fallback", "_generate_local_cached", "_inline_feedback", - "_is_default_ollama", + "_is_default_local", + "_is_gguf_file", "_is_local", "_local_candidates", "_local_executor_dir", @@ -5430,7 +5436,7 @@ "planner_complete", "planner_info" ], - "importance": 0.28 + "importance": 0.48 }, { "path": "simplicio/runtime_env.py", @@ -10083,21 +10089,23 @@ { "path": "simplicio_cli.egg-info/PKG-INFO", "language": "text", - "size_bytes": 6851, - "last_modified": "2026-06-01T04:46:39.808Z", - "file_hash": "44f0b3f5d37b990a6cd4b310d0a1646115a392373412e5d7bf6c7f1d5d389caa", + "size_bytes": 45849, + "last_modified": "2026-06-01T21:51:54.304Z", + "file_hash": "803450044c94a924b800aa3ba486aa6335e226f79463aab406e4ee6111812e7e", "git_status": "clean", "roles": [], "imports": [], - "exports": [], - "importance": 0.12 + "exports": [ + "do" + ], + "importance": 0.2 }, { "path": "simplicio_cli.egg-info/SOURCES.txt", "language": "txt", - "size_bytes": 19353, - "last_modified": "2026-06-01T04:46:39.869Z", - "file_hash": "aed764732e6516fa3be791fa9d8905c377681abf70c6ef95df0c460517893bd4", + "size_bytes": 20836, + "last_modified": "2026-06-01T21:51:54.352Z", + "file_hash": "a8f1ca1a704b9cebb4a39f23c38a1eb04f9395a9aaf42ab6adcb2860c36ae4d7", "git_status": "clean", "roles": [], "imports": [], @@ -10108,7 +10116,7 @@ "path": "simplicio_cli.egg-info/dependency_links.txt", "language": "txt", "size_bytes": 1, - "last_modified": "2026-06-01T04:46:39.809Z", + "last_modified": "2026-06-01T21:51:54.305Z", "file_hash": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", "git_status": "clean", "roles": [], @@ -10120,7 +10128,7 @@ "path": "simplicio_cli.egg-info/entry_points.txt", "language": "txt", "size_bytes": 49, - "last_modified": "2026-06-01T04:46:39.809Z", + "last_modified": "2026-06-01T21:51:54.305Z", "file_hash": "7be4073724468cc63cc0276611c5262a6cca5d9de8c530d3a144132a582142c1", "git_status": "clean", "roles": [], @@ -10132,8 +10140,8 @@ "path": "simplicio_cli.egg-info/requires.txt", "language": "txt", "size_bytes": 264, - "last_modified": "2026-06-01T04:46:39.809Z", - "file_hash": "66dc71379c18d547596c9f40542d81a2d5a347aa6c2f641c9be10f7af6ae58c5", + "last_modified": "2026-06-01T21:51:54.305Z", + "file_hash": "b7a0d71142c03f291f38af056b41d8a1ebb53d151386f7b4d7e8a1227650a023", "git_status": "clean", "roles": [], "imports": [], @@ -10144,7 +10152,7 @@ "path": "simplicio_cli.egg-info/top_level.txt", "language": "txt", "size_bytes": 10, - "last_modified": "2026-06-01T04:46:39.810Z", + "last_modified": "2026-06-01T21:51:54.305Z", "file_hash": "a83097ab8f6c39ccf6f0a37bc47c132a186c5eea99197ae7278d1b8c0d8ecb57", "git_status": "clean", "roles": [], @@ -10490,10 +10498,10 @@ { "path": "tests/python/test_local_models.py", "language": "python", - "size_bytes": 8288, - "last_modified": "2026-06-01T03:12:14.831Z", - "file_hash": "39aec44b202f3b0cae64c8951b72b3224edc807c4793f46e2643ce34cc97bbc7", - "git_status": "clean", + "size_bytes": 8630, + "last_modified": "2026-06-01T21:57:24.384Z", + "file_hash": "8f5fbd6065e99b9db9654c3b891cde865ff401e54963ff97371bace4546af495", + "git_status": "M", "roles": [ "domain", "test" @@ -10506,19 +10514,19 @@ ], "exports": [ "_profile", - "fake_pull", - "test_apple_silicon_profile_can_run_minicpm5_at_24gb", - "test_ensure_recommended_does_not_pull_without_opt_in", - "test_ensure_recommended_pulls_via_env_var", - "test_ensure_recommended_pulls_with_explicit_opt_in", - "test_ensure_recommended_refuses_pull_when_undersized", - "test_ensure_recommended_skips_pull_when_already_installed", - "test_evaluate_marks_ollama_absent", + "fake_download", + "test_apple_silicon_profile_can_run_qwen_gguf_at_24gb", + "test_ensure_recommended_does_not_download_without_opt_in", + "test_ensure_recommended_downloads_via_env_var", + "test_ensure_recommended_downloads_with_explicit_opt_in", + "test_ensure_recommended_refuses_download_when_undersized", + "test_ensure_recommended_skips_download_when_already_installed", + "test_evaluate_marks_gguf_absent", "test_evaluate_picks_correct_spec_per_tier", "test_evaluate_refuses_to_run_oversized_model", "test_pick_tier_threshold_table" ], - "importance": 0.73 + "importance": 0.93 }, { "path": "tests/python/test_mapping_retry_flow.py", @@ -10602,8 +10610,8 @@ "path": "tests/python/test_package_metadata.py", "language": "python", "size_bytes": 660, - "last_modified": "2026-06-01T05:04:04.188Z", - "file_hash": "0dad18f119b1091d5da99c789b42a8e811ed40a8acbc63821f044682bc907711", + "last_modified": "2026-06-01T21:50:25.294Z", + "file_hash": "dbbc1e6a7b3410ee9810ee93f0f5bcb1b1bfcbb1cd7e11c032a6b4650f63bd8f", "git_status": "M", "roles": [ "test" @@ -10682,15 +10690,16 @@ { "path": "tests/python/test_providers_local.py", "language": "python", - "size_bytes": 13645, - "last_modified": "2026-06-01T03:11:50.226Z", - "file_hash": "dd6710d790904c4610b7bc9463317c3b61ff6ddd1761024ff64bc6c9b506d642", + "size_bytes": 14701, + "last_modified": "2026-06-01T21:45:48.787Z", + "file_hash": "586d25220c057e518aa5885bf88bf04042f76aaa756953cee6e5d289b7b9b0b5", "git_status": "clean", "roles": [ "test" ], "imports": [ "os", + "pathlib", "pytest", "simplicio", "simplicio._cache", @@ -10702,16 +10711,15 @@ "exports": [ "_clean", "_touch", - "fail_openai", "fake_local", "fake_openai", - "test_empty_config_uses_ollama_default_not_in_process_local", + "test_empty_config_uses_in_process_local_default", "test_generate_cache_key_includes_weights", - "test_generate_default_ollama_falls_back_to_qwen_gguf", + "test_generate_default_local_uses_qwen_gguf", "test_generate_local_explicit_prefix", "test_generate_local_uses_completion_cache", "test_generate_no_model_with_base_still_raises", - "test_generate_routes_to_ollama_by_default", + "test_generate_routes_to_local_llama_by_default", "test_info_local_auto_default", "test_info_local_explicit", "test_is_local_explicit_prefix", @@ -10734,16 +10742,17 @@ "test_resolve_local_path_existing_file", "test_resolve_local_path_missing_file_raises", "test_resolve_local_path_no_hf_lib_raises", - "test_resolve_local_path_prefers_executor_dir" + "test_resolve_local_path_prefers_executor_dir", + "test_resolve_local_path_skips_corrupt_executor_file" ], "importance": 0.53 }, { "path": "tests/python/test_providers_shellout.py", "language": "python", - "size_bytes": 6694, - "last_modified": "2026-06-01T03:01:57.347Z", - "file_hash": "a5673480e6da7c68b9955a5214ef044fd9abc42557a2717e50ad98163cd03b91", + "size_bytes": 6615, + "last_modified": "2026-06-01T21:45:20.629Z", + "file_hash": "3f72858dd4aa151168eb4b4a9cef0388bb271fad99b4f009718045d481bf6af6", "git_status": "clean", "roles": [ "test" @@ -10764,7 +10773,7 @@ "test_codex_cli_builds_argv_with_model_then_prompt", "test_info_reports_shell_out_modes", "test_native_path_still_requires_key", - "test_no_config_at_all_routes_to_ollama_default", + "test_no_config_at_all_routes_to_local_llama_default", "test_no_model_with_base_raises_with_hint", "test_shell_out_feedback_inlined_into_prompt", "test_shell_out_nonzero_exit_raises_with_stderr", @@ -10838,10 +10847,10 @@ { "path": "tests/python/test_run_cli.py", "language": "python", - "size_bytes": 22509, - "last_modified": "2026-05-31T20:40:06.233Z", - "file_hash": "d187971bbc4ad7e3ed537677443f88ecb17dbbf9ec81636d10134edceed1ffbd", - "git_status": "clean", + "size_bytes": 22903, + "last_modified": "2026-06-01T21:51:01.308Z", + "file_hash": "610ec0fc63cbcac966de48e6ada8b37fa2d1b13559e964723ca16d3c7d085c7e", + "git_status": "M", "roles": [ "test" ], @@ -10856,10 +10865,12 @@ "_diff", "_true_cmd", "_write", + "fake_doctor_main", "fake_index_repo", "fake_planner", "fake_run_feature", "fake_scratch_main", + "test_doctor_command_delegates_to_local_model_preflight", "test_env_export_quotes_dotenv_values", "test_index_accepts_positional_root", "test_run_ambiguous_goal_requires_scope", @@ -10885,7 +10896,7 @@ "test_status_text_reports_state_and_cost", "write_state" ], - "importance": 0.53 + "importance": 0.73 }, { "path": "tests/python/test_runtime_env.py", @@ -12480,9 +12491,13 @@ "name": "stack", "score": 117 }, + { + "name": "local", + "score": 111 + }, { "name": "executor", - "score": 107 + "score": 109 }, { "name": "bench", @@ -12492,10 +12507,6 @@ "name": "parse", "score": 98 }, - { - "name": "local", - "score": 97 - }, { "name": "results", "score": 97 @@ -12520,6 +12531,10 @@ "name": "load", "score": 80 }, + { + "name": "model", + "score": 80 + }, { "name": "reports", "score": 80 @@ -12536,6 +12551,10 @@ "name": "from", "score": 76 }, + { + "name": "path", + "score": 76 + }, { "name": "health", "score": 74 @@ -12544,14 +12563,6 @@ "name": "add", "score": 73 }, - { - "name": "model", - "score": 72 - }, - { - "name": "path", - "score": 72 - }, { "name": "route", "score": 72 @@ -12573,7 +12584,7 @@ "score": 68 }, { - "name": "python", + "name": "file", "score": 66 } ], @@ -12614,83 +12625,67 @@ "status": "M" }, { - "path": "READMEs/README.ar-SA.md", - "status": "M" - }, - { - "path": "READMEs/README.en.md", - "status": "M" - }, - { - "path": "READMEs/README.es-ES.md", - "status": "M" - }, - { - "path": "READMEs/README.fr-FR.md", - "status": "M" - }, - { - "path": "READMEs/README.he-IL.md", + "path": "docs/LLM_USAGE_POLICY.md", "status": "M" }, { - "path": "READMEs/README.hi-IN.md", + "path": "docs/PYTHON_PACKAGE_INTERDEPENDENCE.md", "status": "M" }, { - "path": "READMEs/README.id-ID.md", + "path": "docs/agent-architecture.md", "status": "M" }, { - "path": "READMEs/README.it-IT.md", + "path": "pyproject.toml", "status": "M" }, { - "path": "READMEs/README.ja-JP.md", + "path": "simplicio/__init__.py", "status": "M" }, { - "path": "READMEs/README.ko-KR.md", + "path": "simplicio/cli.py", "status": "M" }, { - "path": "READMEs/README.ms-MY.md", + "path": "simplicio/doctor.py", "status": "M" }, { - "path": "READMEs/README.pl-PL.md", + "path": "simplicio/local_models.py", "status": "M" }, { - "path": "READMEs/README.pt-BR.md", + "path": "simplicio/providers.py", "status": "M" }, { - "path": "READMEs/README.ru-RU.md", - "status": "M" + "path": "simplicio_cli.egg-info/PKG-INFO", + "status": "modified" }, { - "path": "READMEs/README.zh-CN.md", - "status": "M" + "path": "simplicio_cli.egg-info/SOURCES.txt", + "status": "modified" }, { - "path": "docs/PYTHON_PACKAGE_INTERDEPENDENCE.md", + "path": "tests/python/test_local_models.py", "status": "M" }, { - "path": "docs/readme-globalization-standard.md", + "path": "tests/python/test_package_metadata.py", "status": "M" }, { - "path": "pyproject.toml", - "status": "M" + "path": "tests/python/test_providers_local.py", + "status": "modified" }, { - "path": "simplicio/__init__.py", - "status": "M" + "path": "tests/python/test_providers_shellout.py", + "status": "modified" }, { - "path": "tests/python/test_package_metadata.py", + "path": "tests/python/test_run_cli.py", "status": "M" } ], @@ -12698,26 +12693,22 @@ "CHANGELOG.md", "README.md", "README.pt-BR.md", - "READMEs/README.ar-SA.md", - "READMEs/README.en.md", - "READMEs/README.es-ES.md", - "READMEs/README.fr-FR.md", - "READMEs/README.he-IL.md", - "READMEs/README.hi-IN.md", - "READMEs/README.id-ID.md", - "READMEs/README.it-IT.md", - "READMEs/README.ja-JP.md", - "READMEs/README.ko-KR.md", - "READMEs/README.ms-MY.md", - "READMEs/README.pl-PL.md", - "READMEs/README.pt-BR.md", - "READMEs/README.ru-RU.md", - "READMEs/README.zh-CN.md", + "docs/LLM_USAGE_POLICY.md", "docs/PYTHON_PACKAGE_INTERDEPENDENCE.md", - "docs/readme-globalization-standard.md", + "docs/agent-architecture.md", "pyproject.toml", "simplicio/__init__.py", - "tests/python/test_package_metadata.py" + "simplicio/cli.py", + "simplicio/doctor.py", + "simplicio/local_models.py", + "simplicio/providers.py", + "simplicio_cli.egg-info/PKG-INFO", + "simplicio_cli.egg-info/SOURCES.txt", + "tests/python/test_local_models.py", + "tests/python/test_package_metadata.py", + "tests/python/test_providers_local.py", + "tests/python/test_providers_shellout.py", + "tests/python/test_run_cli.py" ], "integration": { "dev_cli_mapper": "read .simplicio/project-map.json, then use .simplicio/precedent-index.json for task-specific examples", diff --git a/.simplicio/symbol-index.json b/.simplicio/symbol-index.json index d0cbc1b..9389b5e 100644 --- a/.simplicio/symbol-index.json +++ b/.simplicio/symbol-index.json @@ -1,7 +1,7 @@ { "schema": "simplicio.symbol-index/v1", "version": 1, - "generated_at": "2026-06-01T05:04:23.593Z", + "generated_at": "2026-06-01T21:57:57.150Z", "root": "/Users/wesleysimplicio/Projetos/ai/simplicio-dev-cli", "symbols": [ { @@ -7354,10 +7354,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/doctor.py", - "line": 21, + "line": 19, "evidence": { "file": "simplicio/doctor.py", - "line": 21 + "line": 19 } }, { @@ -7366,10 +7366,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/doctor.py", - "line": 71, + "line": 64, "evidence": { "file": "simplicio/doctor.py", - "line": 71 + "line": 64 } }, { @@ -7906,34 +7906,70 @@ "kind": "class", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 29, + "line": 40, "evidence": { "file": "simplicio/local_models.py", - "line": 29 + "line": 40 } }, { - "name": "ollama_present", - "qualified_name": "simplicio/local_models.py::ollama_present", + "name": "ollama_id", + "qualified_name": "simplicio/local_models.py::ollama_id", "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 60, + "line": 50, "evidence": { "file": "simplicio/local_models.py", - "line": 60 + "line": 50 } }, { - "name": "ollama_list_installed", - "qualified_name": "simplicio/local_models.py::ollama_list_installed", + "name": "local_model_dir", + "qualified_name": "simplicio/local_models.py::local_model_dir", "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 64, + "line": 67, "evidence": { "file": "simplicio/local_models.py", - "line": 64 + "line": 67 + } + }, + { + "name": "model_file_path", + "qualified_name": "simplicio/local_models.py::model_file_path", + "kind": "function", + "language": "python", + "defined_in": "simplicio/local_models.py", + "line": 71, + "evidence": { + "file": "simplicio/local_models.py", + "line": 71 + } + }, + { + "name": "_is_gguf_file", + "qualified_name": "simplicio/local_models.py::_is_gguf_file", + "kind": "function", + "language": "python", + "defined_in": "simplicio/local_models.py", + "line": 78, + "evidence": { + "file": "simplicio/local_models.py", + "line": 78 + } + }, + { + "name": "model_file_present", + "qualified_name": "simplicio/local_models.py::model_file_present", + "kind": "function", + "language": "python", + "defined_in": "simplicio/local_models.py", + "line": 86, + "evidence": { + "file": "simplicio/local_models.py", + "line": 86 } }, { @@ -7942,22 +7978,22 @@ "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 92, + "line": 90, "evidence": { "file": "simplicio/local_models.py", - "line": 92 + "line": 90 } }, { - "name": "pull", - "qualified_name": "simplicio/local_models.py::pull", + "name": "download", + "qualified_name": "simplicio/local_models.py::download", "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 98, + "line": 100, "evidence": { "file": "simplicio/local_models.py", - "line": 98 + "line": 100 } }, { @@ -7966,10 +8002,22 @@ "kind": "class", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 122, + "line": 130, "evidence": { "file": "simplicio/local_models.py", - "line": 122 + "line": 130 + } + }, + { + "name": "can_pull", + "qualified_name": "simplicio/local_models.py::can_pull", + "kind": "function", + "language": "python", + "defined_in": "simplicio/local_models.py", + "line": 139, + "evidence": { + "file": "simplicio/local_models.py", + "line": 139 } }, { @@ -7978,10 +8026,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 129, + "line": 142, "evidence": { "file": "simplicio/local_models.py", - "line": 129 + "line": 142 } }, { @@ -7990,10 +8038,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 152, + "line": 168, "evidence": { "file": "simplicio/local_models.py", - "line": 152 + "line": 168 } }, { @@ -8002,10 +8050,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/local_models.py", - "line": 182, + "line": 201, "evidence": { "file": "simplicio/local_models.py", - "line": 182 + "line": 201 } }, { @@ -8914,10 +8962,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 46, + "line": 44, "evidence": { "file": "simplicio/providers.py", - "line": 46 + "line": 44 } }, { @@ -8926,10 +8974,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 56, + "line": 54, "evidence": { "file": "simplicio/providers.py", - "line": 56 + "line": 54 } }, { @@ -8938,10 +8986,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 68, + "line": 66, "evidence": { "file": "simplicio/providers.py", - "line": 68 + "line": 66 } }, { @@ -8950,22 +8998,22 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 94, + "line": 89, "evidence": { "file": "simplicio/providers.py", - "line": 94 + "line": 89 } }, { - "name": "_is_default_ollama", - "qualified_name": "simplicio/providers.py::_is_default_ollama", + "name": "_is_default_local", + "qualified_name": "simplicio/providers.py::_is_default_local", "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 105, + "line": 100, "evidence": { "file": "simplicio/providers.py", - "line": 105 + "line": 100 } }, { @@ -8974,10 +9022,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 115, + "line": 107, "evidence": { "file": "simplicio/providers.py", - "line": 115 + "line": 107 } }, { @@ -8986,10 +9034,22 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 143, + "line": 135, "evidence": { "file": "simplicio/providers.py", - "line": 143 + "line": 135 + } + }, + { + "name": "_is_gguf_file", + "qualified_name": "simplicio/providers.py::_is_gguf_file", + "kind": "function", + "language": "python", + "defined_in": "simplicio/providers.py", + "line": 139, + "evidence": { + "file": "simplicio/providers.py", + "line": 139 } }, { @@ -8998,10 +9058,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 147, + "line": 148, "evidence": { "file": "simplicio/providers.py", - "line": 147 + "line": 148 } }, { @@ -9010,10 +9070,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 152, + "line": 153, "evidence": { "file": "simplicio/providers.py", - "line": 152 + "line": 153 } }, { @@ -9022,10 +9082,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 181, + "line": 192, "evidence": { "file": "simplicio/providers.py", - "line": 181 + "line": 192 } }, { @@ -9034,10 +9094,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 210, + "line": 221, "evidence": { "file": "simplicio/providers.py", - "line": 210 + "line": 221 } }, { @@ -9046,10 +9106,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 224, + "line": 235, "evidence": { "file": "simplicio/providers.py", - "line": 224 + "line": 235 } }, { @@ -9058,10 +9118,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 236, + "line": 247, "evidence": { "file": "simplicio/providers.py", - "line": 236 + "line": 247 } }, { @@ -9070,10 +9130,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 273, + "line": 284, "evidence": { "file": "simplicio/providers.py", - "line": 273 + "line": 284 } }, { @@ -9082,10 +9142,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 282, + "line": 293, "evidence": { "file": "simplicio/providers.py", - "line": 282 + "line": 293 } }, { @@ -9094,10 +9154,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 289, + "line": 300, "evidence": { "file": "simplicio/providers.py", - "line": 289 + "line": 300 } }, { @@ -9106,10 +9166,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 298, + "line": 309, "evidence": { "file": "simplicio/providers.py", - "line": 298 + "line": 309 } }, { @@ -9118,10 +9178,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 304, + "line": 315, "evidence": { "file": "simplicio/providers.py", - "line": 304 + "line": 315 } }, { @@ -9130,22 +9190,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 330, - "evidence": { - "file": "simplicio/providers.py", - "line": 330 - } - }, - { - "name": "_generate_default_ollama_with_fallback", - "qualified_name": "simplicio/providers.py::_generate_default_ollama_with_fallback", - "kind": "function", - "language": "python", - "defined_in": "simplicio/providers.py", - "line": 340, + "line": 341, "evidence": { "file": "simplicio/providers.py", - "line": 340 + "line": 341 } }, { @@ -9154,10 +9202,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 386, + "line": 351, "evidence": { "file": "simplicio/providers.py", - "line": 386 + "line": 351 } }, { @@ -9166,10 +9214,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 478, + "line": 447, "evidence": { "file": "simplicio/providers.py", - "line": 478 + "line": 447 } }, { @@ -9178,10 +9226,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 544, + "line": 515, "evidence": { "file": "simplicio/providers.py", - "line": 544 + "line": 515 } }, { @@ -9190,10 +9238,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 605, + "line": 576, "evidence": { "file": "simplicio/providers.py", - "line": 605 + "line": 576 } }, { @@ -9202,10 +9250,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 620, + "line": 591, "evidence": { "file": "simplicio/providers.py", - "line": 620 + "line": 591 } }, { @@ -9214,10 +9262,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 631, + "line": 602, "evidence": { "file": "simplicio/providers.py", - "line": 631 + "line": 602 } }, { @@ -9226,10 +9274,10 @@ "kind": "function", "language": "python", "defined_in": "simplicio/providers.py", - "line": 713, + "line": 684, "evidence": { "file": "simplicio/providers.py", - "line": 713 + "line": 684 } }, { @@ -15178,154 +15226,154 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 59, + "line": 60, "evidence": { "file": "tests/python/test_local_models.py", - "line": 59 + "line": 60 } }, { - "name": "test_evaluate_marks_ollama_absent", - "qualified_name": "tests/python/test_local_models.py::test_evaluate_marks_ollama_absent", + "name": "test_evaluate_marks_gguf_absent", + "qualified_name": "tests/python/test_local_models.py::test_evaluate_marks_gguf_absent", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 81, + "line": 82, "evidence": { "file": "tests/python/test_local_models.py", - "line": 81 + "line": 82 } }, { - "name": "test_apple_silicon_profile_can_run_minicpm5_at_24gb", - "qualified_name": "tests/python/test_local_models.py::test_apple_silicon_profile_can_run_minicpm5_at_24gb", + "name": "test_apple_silicon_profile_can_run_qwen_gguf_at_24gb", + "qualified_name": "tests/python/test_local_models.py::test_apple_silicon_profile_can_run_qwen_gguf_at_24gb", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 89, + "line": 90, "evidence": { "file": "tests/python/test_local_models.py", - "line": 89 + "line": 90 } }, { - "name": "test_ensure_recommended_does_not_pull_without_opt_in", - "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_does_not_pull_without_opt_in", + "name": "test_ensure_recommended_does_not_download_without_opt_in", + "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_does_not_download_without_opt_in", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 106, + "line": 107, "evidence": { "file": "tests/python/test_local_models.py", - "line": 106 + "line": 107 } }, { - "name": "fake_pull", - "qualified_name": "tests/python/test_local_models.py::fake_pull", + "name": "fake_download", + "qualified_name": "tests/python/test_local_models.py::fake_download", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 124, + "line": 126, "evidence": { "file": "tests/python/test_local_models.py", - "line": 124 + "line": 126 } }, { - "name": "test_ensure_recommended_pulls_with_explicit_opt_in", - "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_pulls_with_explicit_opt_in", + "name": "test_ensure_recommended_downloads_with_explicit_opt_in", + "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_downloads_with_explicit_opt_in", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 138, + "line": 140, "evidence": { "file": "tests/python/test_local_models.py", - "line": 138 + "line": 140 } }, { - "name": "fake_pull", - "qualified_name": "tests/python/test_local_models.py::fake_pull", + "name": "fake_download", + "qualified_name": "tests/python/test_local_models.py::fake_download", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 147, + "line": 149, "evidence": { "file": "tests/python/test_local_models.py", - "line": 147 + "line": 149 } }, { - "name": "test_ensure_recommended_pulls_via_env_var", - "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_pulls_via_env_var", + "name": "test_ensure_recommended_downloads_via_env_var", + "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_downloads_via_env_var", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 162, + "line": 164, "evidence": { "file": "tests/python/test_local_models.py", - "line": 162 + "line": 164 } }, { - "name": "fake_pull", - "qualified_name": "tests/python/test_local_models.py::fake_pull", + "name": "fake_download", + "qualified_name": "tests/python/test_local_models.py::fake_download", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 172, + "line": 174, "evidence": { "file": "tests/python/test_local_models.py", - "line": 172 + "line": 174 } }, { - "name": "test_ensure_recommended_skips_pull_when_already_installed", - "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_skips_pull_when_already_installed", + "name": "test_ensure_recommended_skips_download_when_already_installed", + "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_skips_download_when_already_installed", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 184, + "line": 186, "evidence": { "file": "tests/python/test_local_models.py", - "line": 184 + "line": 186 } }, { - "name": "fake_pull", - "qualified_name": "tests/python/test_local_models.py::fake_pull", + "name": "fake_download", + "qualified_name": "tests/python/test_local_models.py::fake_download", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 193, + "line": 194, "evidence": { "file": "tests/python/test_local_models.py", - "line": 193 + "line": 194 } }, { - "name": "test_ensure_recommended_refuses_pull_when_undersized", - "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_refuses_pull_when_undersized", + "name": "test_ensure_recommended_refuses_download_when_undersized", + "qualified_name": "tests/python/test_local_models.py::test_ensure_recommended_refuses_download_when_undersized", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 206, + "line": 207, "evidence": { "file": "tests/python/test_local_models.py", - "line": 206 + "line": 207 } }, { - "name": "fake_pull", - "qualified_name": "tests/python/test_local_models.py::fake_pull", + "name": "fake_download", + "qualified_name": "tests/python/test_local_models.py::fake_download", "kind": "function", "language": "python", "defined_in": "tests/python/test_local_models.py", - "line": 222, + "line": 223, "evidence": { "file": "tests/python/test_local_models.py", - "line": 222 + "line": 223 } }, { @@ -16313,8 +16361,8 @@ } }, { - "name": "test_empty_config_uses_ollama_default_not_in_process_local", - "qualified_name": "tests/python/test_providers_local.py::test_empty_config_uses_ollama_default_not_in_process_local", + "name": "test_empty_config_uses_in_process_local_default", + "qualified_name": "tests/python/test_providers_local.py::test_empty_config_uses_in_process_local_default", "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", @@ -16510,10 +16558,22 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 172, + "line": 178, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 172 + "line": 178 + } + }, + { + "name": "test_resolve_local_path_skips_corrupt_executor_file", + "qualified_name": "tests/python/test_providers_local.py::test_resolve_local_path_skips_corrupt_executor_file", + "kind": "function", + "language": "python", + "defined_in": "tests/python/test_providers_local.py", + "line": 196, + "evidence": { + "file": "tests/python/test_providers_local.py", + "line": 196 } }, { @@ -16522,10 +16582,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 190, + "line": 217, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 190 + "line": 217 } }, { @@ -16534,10 +16594,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 213, + "line": 240, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 213 + "line": 240 } }, { @@ -16546,10 +16606,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 226, + "line": 253, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 226 + "line": 253 } }, { @@ -16558,10 +16618,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 234, + "line": 261, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 234 + "line": 261 } }, { @@ -16570,22 +16630,22 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 252, + "line": 279, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 252 + "line": 279 } }, { - "name": "test_generate_routes_to_ollama_by_default", - "qualified_name": "tests/python/test_providers_local.py::test_generate_routes_to_ollama_by_default", + "name": "test_generate_routes_to_local_llama_by_default", + "qualified_name": "tests/python/test_providers_local.py::test_generate_routes_to_local_llama_by_default", "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 274, + "line": 301, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 274 + "line": 301 } }, { @@ -16594,34 +16654,34 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 278, + "line": 305, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 278 + "line": 305 } }, { - "name": "test_generate_default_ollama_falls_back_to_qwen_gguf", - "qualified_name": "tests/python/test_providers_local.py::test_generate_default_ollama_falls_back_to_qwen_gguf", + "name": "fake_local", + "qualified_name": "tests/python/test_providers_local.py::fake_local", "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 288, + "line": 308, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 288 + "line": 308 } }, { - "name": "fail_openai", - "qualified_name": "tests/python/test_providers_local.py::fail_openai", + "name": "test_generate_default_local_uses_qwen_gguf", + "qualified_name": "tests/python/test_providers_local.py::test_generate_default_local_uses_qwen_gguf", "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 292, + "line": 319, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 292 + "line": 319 } }, { @@ -16630,10 +16690,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 295, + "line": 323, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 295 + "line": 323 } }, { @@ -16642,10 +16702,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 305, + "line": 332, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 305 + "line": 332 } }, { @@ -16654,10 +16714,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 309, + "line": 336, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 309 + "line": 336 } }, { @@ -16666,10 +16726,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 318, + "line": 345, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 318 + "line": 345 } }, { @@ -16678,10 +16738,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 322, + "line": 349, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 322 + "line": 349 } }, { @@ -16690,10 +16750,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 332, + "line": 359, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 332 + "line": 359 } }, { @@ -16702,10 +16762,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 340, + "line": 367, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 340 + "line": 367 } }, { @@ -16714,10 +16774,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 356, + "line": 383, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 356 + "line": 383 } }, { @@ -16726,10 +16786,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 361, + "line": 388, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 361 + "line": 388 } }, { @@ -16738,10 +16798,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_local.py", - "line": 378, + "line": 405, "evidence": { "file": "tests/python/test_providers_local.py", - "line": 378 + "line": 405 } }, { @@ -16889,8 +16949,8 @@ } }, { - "name": "test_no_config_at_all_routes_to_ollama_default", - "qualified_name": "tests/python/test_providers_shellout.py::test_no_config_at_all_routes_to_ollama_default", + "name": "test_no_config_at_all_routes_to_local_llama_default", + "qualified_name": "tests/python/test_providers_shellout.py::test_no_config_at_all_routes_to_local_llama_default", "kind": "function", "language": "python", "defined_in": "tests/python/test_providers_shellout.py", @@ -17249,8 +17309,8 @@ } }, { - "name": "test_run_auto_task_infers_target_from_goal", - "qualified_name": "tests/python/test_run_cli.py::test_run_auto_task_infers_target_from_goal", + "name": "test_doctor_command_delegates_to_local_model_preflight", + "qualified_name": "tests/python/test_run_cli.py::test_doctor_command_delegates_to_local_model_preflight", "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", @@ -17260,16 +17320,40 @@ "line": 92 } }, + { + "name": "fake_doctor_main", + "qualified_name": "tests/python/test_run_cli.py::fake_doctor_main", + "kind": "function", + "language": "python", + "defined_in": "tests/python/test_run_cli.py", + "line": 97, + "evidence": { + "file": "tests/python/test_run_cli.py", + "line": 97 + } + }, + { + "name": "test_run_auto_task_infers_target_from_goal", + "qualified_name": "tests/python/test_run_cli.py::test_run_auto_task_infers_target_from_goal", + "kind": "function", + "language": "python", + "defined_in": "tests/python/test_run_cli.py", + "line": 108, + "evidence": { + "file": "tests/python/test_run_cli.py", + "line": 108 + } + }, { "name": "test_run_ambiguous_goal_requires_scope", "qualified_name": "tests/python/test_run_cli.py::test_run_ambiguous_goal_requires_scope", "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 113, + "line": 129, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 113 + "line": 129 } }, { @@ -17278,10 +17362,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 122, + "line": 138, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 122 + "line": 138 } }, { @@ -17290,10 +17374,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 127, + "line": 143, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 127 + "line": 143 } }, { @@ -17302,10 +17386,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 159, + "line": 175, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 159 + "line": 175 } }, { @@ -17314,10 +17398,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 164, + "line": 180, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 164 + "line": 180 } }, { @@ -17326,10 +17410,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 192, + "line": 208, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 192 + "line": 208 } }, { @@ -17338,10 +17422,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 196, + "line": 212, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 196 + "line": 212 } }, { @@ -17350,10 +17434,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 227, + "line": 243, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 227 + "line": 243 } }, { @@ -17362,10 +17446,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 239, + "line": 255, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 239 + "line": 255 } }, { @@ -17374,10 +17458,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 286, + "line": 302, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 286 + "line": 302 } }, { @@ -17386,10 +17470,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 309, + "line": 325, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 309 + "line": 325 } }, { @@ -17398,10 +17482,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 332, + "line": 348, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 332 + "line": 348 } }, { @@ -17410,10 +17494,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 350, + "line": 366, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 350 + "line": 366 } }, { @@ -17422,10 +17506,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 378, + "line": 394, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 378 + "line": 394 } }, { @@ -17434,10 +17518,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 409, + "line": 425, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 409 + "line": 425 } }, { @@ -17446,10 +17530,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 422, + "line": 438, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 422 + "line": 438 } }, { @@ -17458,10 +17542,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 466, + "line": 482, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 466 + "line": 482 } }, { @@ -17470,10 +17554,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 475, + "line": 491, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 475 + "line": 491 } }, { @@ -17482,10 +17566,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 487, + "line": 503, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 487 + "line": 503 } }, { @@ -17494,10 +17578,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 491, + "line": 507, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 491 + "line": 507 } }, { @@ -17506,10 +17590,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 524, + "line": 540, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 524 + "line": 540 } }, { @@ -17518,10 +17602,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 556, + "line": 572, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 556 + "line": 572 } }, { @@ -17530,10 +17614,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 585, + "line": 601, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 585 + "line": 601 } }, { @@ -17542,10 +17626,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 626, + "line": 642, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 626 + "line": 642 } }, { @@ -17554,10 +17638,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 662, + "line": 678, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 662 + "line": 678 } }, { @@ -17566,10 +17650,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 707, + "line": 723, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 707 + "line": 723 } }, { @@ -17578,10 +17662,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 746, + "line": 762, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 746 + "line": 762 } }, { @@ -17590,10 +17674,10 @@ "kind": "function", "language": "python", "defined_in": "tests/python/test_run_cli.py", - "line": 757, + "line": 773, "evidence": { "file": "tests/python/test_run_cli.py", - "line": 757 + "line": 773 } }, { @@ -21666,7 +21750,7 @@ } ], "counts": { - "symbols": 1805, + "symbols": 1812, "files": 185 } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfd1df..b4229f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.5.17] — 2026-06-01 + +### Changed +- Changed the no-config and `--local` execution default to `local-llama/default` + via `llama-cpp-python`, removing Ollama from the local default path. +- Updated `simplicio doctor` to validate/download the default + `Qwen_Qwen3.5-2B-Q6_K.gguf` GGUF model instead of checking/pulling Ollama. +- Added GGUF header validation so corrupt local model files are not silently + reused. + ## [0.5.16] — 2026-06-01 ### Changed diff --git a/README.md b/README.md index b0e9f16..b4b8796 100644 --- a/README.md +++ b/README.md @@ -610,8 +610,7 @@ user prompt. UserPromptSubmit is the right pre-hook for routing decisions. | GLM (z.ai) | `glm-4.6` | `https://api.z.ai/api/paas/v4` | | DeepSeek | `deepseek-chat` | `https://api.deepseek.com` | | OpenAI | `gpt-4.1` | `https://api.openai.com/v1` | -| Local (Ollama) | `openbmb/minicpm5:latest` | `http://localhost:11434/v1` | -| Local (in-process) | `local-llama/default` | *(leave unset)* | +| Local (llama.cpp) | `local-llama/default` | *(leave unset)* | | Anthropic native | `claude-opus-4-7` | *(leave unset)* | If `SIMPLICIO_BASE_URL` is unset and the key is `ANTHROPIC_API_KEY`, it uses the @@ -622,30 +621,28 @@ your `base_url` — so **any** OpenAI-like provider works without code changes. simplicio smoke # prints provider config + one test call ``` -#### Path 4 — local Ollama primary with GGUF fallback +#### Path 4 — local llama.cpp GGUF default When **no provider is configured** (`SIMPLICIO_MODEL` and -`SIMPLICIO_BASE_URL` both unset), simplicio uses local Ollama with -`openbmb/minicpm5:latest`. If that call fails, it falls back to the in-process +`SIMPLICIO_BASE_URL` both unset), simplicio runs the in-process [`llama-cpp-python`](https://github.com/abetlen/llama-cpp-python) backend with -`Qwen_Qwen3.5-2B-Q6_K.gguf`. +`local-llama/default`, currently +`bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf`. ```bash pip install 'simplicio-cli[local]' # pulls llama-cpp-python + huggingface-hub +simplicio doctor --install # downloads/validates the default GGUF simplicio task "add input validation to createUser" \ - --target src/users.ts --local # forces local Ollama primary + --target src/users.ts --local # forces local llama.cpp -# the fallback GGUF is fetched once from the Hugging Face Hub, then reused +# the GGUF is fetched once from the Hugging Face Hub, then reused ``` Explicit routes (override the default model/weights): ```bash -SIMPLICIO_MODEL=openbmb/minicpm5:latest -SIMPLICIO_BASE_URL=http://localhost:11434/v1 -SIMPLICIO_API_KEY=ollama -SIMPLICIO_MODEL=local-llama/default # Qwen_Qwen3.5-2B-Q6_K.gguf fallback +SIMPLICIO_MODEL=local-llama/default # Qwen_Qwen3.5-2B-Q6_K.gguf default SIMPLICIO_MODEL=local-llama/bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf SIMPLICIO_MODEL=local-llama//models/my-model.gguf # direct local path SIMPLICIO_LOCAL_MODEL_PATH=/models/my-model.gguf # always wins diff --git a/README.pt-BR.md b/README.pt-BR.md index 8fb7673..de0c48a 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -610,8 +610,7 @@ user prompt. UserPromptSubmit is the right pre-hook for routing decisions. | GLM (z.ai) | `glm-4.6` | `https://api.z.ai/api/paas/v4` | | DeepSeek | `deepseek-chat` | `https://api.deepseek.com` | | OpenAI | `gpt-4.1` | `https://api.openai.com/v1` | -| Local (Ollama) | `openbmb/minicpm5:latest` | `http://localhost:11434/v1` | -| Local (in-process) | `local-llama/default` | *(leave unset)* | +| Local (llama.cpp) | `local-llama/default` | *(leave unset)* | | Anthropic native | `claude-opus-4-7` | *(leave unset)* | If `SIMPLICIO_BASE_URL` is unset and the key is `ANTHROPIC_API_KEY`, it uses the @@ -622,30 +621,28 @@ your `base_url` — so **any** OpenAI-like provider works without code changes. simplicio smoke # prints provider config + one test call ``` -#### Path 4 — local Ollama primary with GGUF fallback +#### Path 4 — local llama.cpp GGUF default When **no provider is configured** (`SIMPLICIO_MODEL` and -`SIMPLICIO_BASE_URL` both unset), simplicio uses local Ollama with -`openbmb/minicpm5:latest`. If that call fails, it falls back to the in-process +`SIMPLICIO_BASE_URL` both unset), simplicio runs the in-process [`llama-cpp-python`](https://github.com/abetlen/llama-cpp-python) backend with -`Qwen_Qwen3.5-2B-Q6_K.gguf`. +`local-llama/default`, currently +`bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf`. ```bash pip install 'simplicio-cli[local]' # pulls llama-cpp-python + huggingface-hub +simplicio doctor --install # downloads/validates the default GGUF simplicio task "add input validation to createUser" \ - --target src/users.ts --local # forces local Ollama primary + --target src/users.ts --local # forces local llama.cpp -# the fallback GGUF is fetched once from the Hugging Face Hub, then reused +# the GGUF is fetched once from the Hugging Face Hub, then reused ``` Explicit routes (override the default model/weights): ```bash -SIMPLICIO_MODEL=openbmb/minicpm5:latest -SIMPLICIO_BASE_URL=http://localhost:11434/v1 -SIMPLICIO_API_KEY=ollama -SIMPLICIO_MODEL=local-llama/default # Qwen_Qwen3.5-2B-Q6_K.gguf fallback +SIMPLICIO_MODEL=local-llama/default # Qwen_Qwen3.5-2B-Q6_K.gguf default SIMPLICIO_MODEL=local-llama/bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf SIMPLICIO_MODEL=local-llama//models/my-model.gguf # direct local path SIMPLICIO_LOCAL_MODEL_PATH=/models/my-model.gguf # always wins diff --git a/docs/LLM_USAGE_POLICY.md b/docs/LLM_USAGE_POLICY.md index 7a168e9..0d4cc94 100644 --- a/docs/LLM_USAGE_POLICY.md +++ b/docs/LLM_USAGE_POLICY.md @@ -10,24 +10,29 @@ ## Default Configuration ### Execution (Local) -- Primary local executor: **`openbmb/minicpm5:latest`** via local Ollama -- Fallback local executor: **`Qwen_Qwen3.5-2B-Q6_K.gguf`** from `bartowski/Qwen_Qwen3.5-2B-GGUF` +- Primary local executor: **`local-llama/default`** via `llama.cpp` / + `llama-cpp-python` +- Default GGUF: **`Qwen_Qwen3.5-2B-Q6_K.gguf`** from + `bartowski/Qwen_Qwen3.5-2B-GGUF` -The GGUF fallback should be used via llama.cpp / llama-cpp-python only when -Ollama is unavailable or the MiniCPM5 call fails. +The local default must not require Ollama or any HTTP service. Remote or +OpenAI-compatible endpoints remain explicit opt-ins via `SIMPLICIO_MODEL`, +`SIMPLICIO_BASE_URL`, and credentials. ## Project-Specific Rules ### simplicio-code (mandatory) - On project bootstrap / SessionStart / first run in a new workspace: - - The system **must** verify that `openbmb/minicpm5:latest` is available in local Ollama. - - The system **must** verify that `Qwen_Qwen3.5-2B-Q6_K.gguf` is present as the fallback file. - - If either is missing, it **must** install it before allowing agent execution. + - The system **must** verify that `Qwen_Qwen3.5-2B-Q6_K.gguf` is present and + has a valid `GGUF` header. + - If it is missing, it **must** download/prepare it before allowing local + agent execution. - This is a hard requirement for the SimplicioCode product. ### simplicio-dev-cli and simplicio-sprint (recommended) -- The above split (`openbmb/minicpm5:latest` primary + Qwen3.5 Q6_K GGUF fallback) is the **recommended** configuration for local development. -- Not enforced at runtime, but all examples, benchmarks, and documentation use this setup. +- The above `local-llama/default` Qwen3.5 Q6_K GGUF setup is the + **recommended** configuration for local development. +- `simplicio doctor` validates this setup at runtime. ## Rationale @@ -39,12 +44,11 @@ From extensive benchmarking (see `simplicio-dev-cli` quant curves and live gates ## How to Configure ```bash -# Execution (local Ollama primary) -export SIMPLICIO_MODEL=openbmb/minicpm5:latest -export SIMPLICIO_BASE_URL=http://localhost:11434/v1 -export SIMPLICIO_API_KEY=ollama +# Execution (local llama.cpp default) +unset SIMPLICIO_MODEL SIMPLICIO_BASE_URL SIMPLICIO_API_KEY +simplicio doctor --install -# fallback explicit route: +# Explicit route: export SIMPLICIO_MODEL=local-llama/bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf ``` @@ -66,3 +70,19 @@ simplicio-dev-cli + simplicio-prompt + agents This combination is the **recommended and documented default** when using `simplicio-dev-cli`. All new examples, benchmarks, and onboarding materials assume this full stack. When starting a new project with the Simplicio starter, the bootstrap configures the environment to use this trio by default. + +## Native Packaged Runtime Direction + +The goal is not to rewrite Simplicio in C++ or Rust. The practical direction is +to package the existing Python implementation as a faster, reproducible native +runtime: + +- a single launcher/binary that bootstraps the pinned Python package, extras, + `llama.cpp` bindings, GGUF path, cache, and mapper state; +- optional Rust/C++ hot-path helpers for process spawning, file locks, task + queues, diff/apply operations, and local-agent scheduling; +- configurable local worker pools, so a `20 agents` request can be accepted by + the interface while the runtime governs safe concurrency for RAM/CPU. + +This keeps the current Python feature velocity while making the mechanical +execution path feel like a program instead of a pile of setup commands. diff --git a/docs/PYTHON_PACKAGE_INTERDEPENDENCE.md b/docs/PYTHON_PACKAGE_INTERDEPENDENCE.md index ff50ef8..2e889c6 100644 --- a/docs/PYTHON_PACKAGE_INTERDEPENDENCE.md +++ b/docs/PYTHON_PACKAGE_INTERDEPENDENCE.md @@ -9,7 +9,7 @@ simplicio-prompt 1.13.3 simplicio-mapper 0.7.3 ^ ^ | | -simplicio-cli 0.5.16 +simplicio-cli 0.5.17 ^ | simplicio-sprint 1.2.11 @@ -27,5 +27,7 @@ simplicio-sprint 1.2.11 ## Local LLM Standard -- Primary: `openbmb/minicpm5:latest` via local Ollama. -- Fallback: `Qwen_Qwen3.5-2B-Q6_K.gguf` via `local-llama/default`. +- Primary: `local-llama/default` via `llama.cpp` / `llama-cpp-python`. +- Default GGUF: `Qwen_Qwen3.5-2B-Q6_K.gguf`. +- Ollama is no longer part of the local default path; it remains an explicit + OpenAI-compatible provider option only when configured by the user. diff --git a/docs/agent-architecture.md b/docs/agent-architecture.md index 6c5dc29..447412c 100644 --- a/docs/agent-architecture.md +++ b/docs/agent-architecture.md @@ -78,7 +78,7 @@ ADR-002 documenta a decisão completa, com alternativas avaliadas e critério de ┌─────────────────────────────────────────────────────────────┐ │ CAMADA 4 — Provedores LLM │ │ ────────────────────────────────────────────────────────── │ -│ Anthropic OpenAI OpenRouter modelos locais (ollama) │ +│ Anthropic OpenAI OpenRouter modelos locais (llama.cpp) │ └─────────────────────────────────────────────────────────────┘ ``` diff --git a/pyproject.toml b/pyproject.toml index 1cdda32..9e913b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simplicio-cli" -version = "0.5.16" +version = "0.5.17" description = "Portable task-to-code pipeline that works with any LLM. Turn a one-line task into a verified code change — diff + test + verify loop. +55 pts on a 156-check benchmark, 21% faster, ~same tokens." readme = "README.md" license = { text = "MIT" } diff --git a/simplicio/__init__.py b/simplicio/__init__.py index 4151b9f..496906a 100644 --- a/simplicio/__init__.py +++ b/simplicio/__init__.py @@ -1 +1 @@ -__version__ = "0.5.16" +__version__ = "0.5.17" diff --git a/simplicio/cli.py b/simplicio/cli.py index 27f9eb3..eabd3da 100644 --- a/simplicio/cli.py +++ b/simplicio/cli.py @@ -91,8 +91,8 @@ def _add_task_args(p: argparse.ArgumentParser, *, target_required: bool) -> None p.add_argument( "--local", action="store_true", - help="force local Ollama (openbmb/minicpm5:latest, no API key) " - "with Qwen3.5 GGUF fallback; overrides SIMPLICIO_MODEL/SIMPLICIO_BASE_URL", + help="force local llama.cpp with Qwen3.5 GGUF; overrides " + "SIMPLICIO_MODEL/SIMPLICIO_BASE_URL", ) @@ -121,11 +121,11 @@ def _add_run_args(p: argparse.ArgumentParser) -> None: def _force_local_if_requested(a: argparse.Namespace) -> None: if getattr(a, "local", False): - # Force Path 4: local Ollama primary. The provider layer falls back to - # the local Qwen GGUF if the Ollama call fails. - os.environ["SIMPLICIO_MODEL"] = "openbmb/minicpm5:latest" - os.environ["SIMPLICIO_BASE_URL"] = "http://localhost:11434/v1" - os.environ.setdefault("SIMPLICIO_API_KEY", "ollama") + # Force Path 4: local in-process llama.cpp. This keeps local execution + # independent from Ollama or any HTTP service. + os.environ["SIMPLICIO_MODEL"] = "local-llama/default" + os.environ.pop("SIMPLICIO_BASE_URL", None) + os.environ.pop("SIMPLICIO_API_KEY", None) def _run_task_command(a: argparse.Namespace) -> int: @@ -609,6 +609,11 @@ def main(argv=None): p_status.add_argument("--root", default=".") p_status.add_argument("--json", action="store_true") + p_doctor = sub.add_parser("doctor", help="check local llama.cpp readiness") + p_doctor.add_argument("--install", action="store_true") + p_doctor.add_argument("--json", action="store_true") + p_doctor.add_argument("--list-tiers", action="store_true") + p_env_export = sub.add_parser( "env-export", help="print shell-safe exports from a dotenv file without sourcing it", @@ -678,6 +683,17 @@ def main(argv=None): return detect_main(detect_argv) elif a.cmd == "status": return _run_status_command(a) + elif a.cmd == "doctor": + from .doctor import main as doctor_main + + doctor_argv = [] + if a.install: + doctor_argv.append("--install") + if a.json: + doctor_argv.append("--json") + if a.list_tiers: + doctor_argv.append("--list-tiers") + return doctor_main(doctor_argv) elif a.cmd == "env-export": from .runtime_env import parse_env_file, shell_export_lines diff --git a/simplicio/doctor.py b/simplicio/doctor.py index c3b9329..5852ead 100644 --- a/simplicio/doctor.py +++ b/simplicio/doctor.py @@ -1,7 +1,7 @@ """doctor.py — `simplicio doctor` subcommand. Prints detected hardware tier + recommended local model + install status. -With --install, opt-in to pulling the recommended model via Ollama. Without +With --install, opt-in to downloading the recommended GGUF. Without the flag, never touches the disk. With --json, machine-readable output. """ from __future__ import annotations @@ -14,9 +14,7 @@ from .local_models import ( RECOMMENDATIONS, ensure_recommended, - is_installed, - ollama_list_installed, - ollama_present, + model_file_path, ) @@ -34,46 +32,41 @@ def _render_human(result, profile) -> None: print() print("recommended doer model:") print(f" label {result.spec.label}") - print(f" ollama id {result.spec.ollama_id}") + print(f" model id {result.spec.model_id}") + print(f" repo {result.spec.repo_id}") + print(f" file {result.spec.filename}") + print(f" path {model_file_path(result.spec)}") print(f" size (Q4) ~{result.spec.size_gb_q4:.1f} GB") print(f" notes {result.spec.notes}") print() - print(f" ollama {'installed' if ollama_present() else 'NOT FOUND on PATH'}") + print(" runtime llama.cpp via llama-cpp-python") print(f" can run {'yes' if result.can_run else 'NO'}") - print(f" can pull {'yes' if result.can_pull else 'NO'}") + print(f" can download {'yes' if result.can_download else 'NO'}") print(f" installed {'YES' if result.installed else 'no'}") if result.reason: print(f" status {result.reason}") print() if result.installed: - print("→ set SIMPLICIO_MODEL to use it:") - print(f" export SIMPLICIO_MODEL={result.spec.ollama_id}") - print(f" export SIMPLICIO_BASE_URL=http://localhost:11434/v1") - print(f" export SIMPLICIO_API_KEY=ollama # any non-empty string works") - elif result.can_pull: - print("→ to install:") + print("-> set SIMPLICIO_MODEL to use it explicitly:") + print(f" export SIMPLICIO_MODEL={result.spec.model_id}") + print(" unset SIMPLICIO_BASE_URL SIMPLICIO_API_KEY") + elif result.can_download: + print("-> to install:") print(" simplicio doctor --install") - print(f" (or manually: ollama pull {result.spec.ollama_id})") - elif not ollama_present(): - print("→ install Ollama first: https://ollama.ai") + print( + f" (or manually download {result.spec.repo_id}/{result.spec.filename} " + f"to {model_file_path(result.spec)})" + ) else: - print("→ hardware is too small for the recommended model; " + print("-> hardware is too small for the recommended model; " "consider a smaller stack or move to cloud (SIMPLICIO_MODEL = " "OpenRouter/HF/etc.)") - other_installed = [m for m in ollama_list_installed() - if m != result.spec.ollama_id] - if other_installed: - print() - print("other Ollama models you have installed:") - for m in other_installed[:10]: - print(f" - {m}") - def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(prog="simplicio doctor") p.add_argument("--install", action="store_true", - help="opt-in: pull the recommended model via Ollama if not present") + help="opt-in: download the recommended GGUF if not present") p.add_argument("--json", action="store_true", help="machine-readable output") p.add_argument("--list-tiers", action="store_true", @@ -83,20 +76,26 @@ def main(argv: list[str] | None = None) -> int: if args.list_tiers: if args.json: print(json.dumps({ - tier: {"ollama_id": s.ollama_id, "label": s.label, - "size_gb_q4": s.size_gb_q4, "notes": s.notes} + tier: { + "model_id": s.model_id, + "repo_id": s.repo_id, + "filename": s.filename, + "label": s.label, + "size_gb_q4": s.size_gb_q4, + "notes": s.notes, + } for tier, s in RECOMMENDATIONS.items() }, indent=2)) else: - print(f"{'tier':14s} {'size':>7s} ollama id") + print(f"{'tier':14s} {'size':>7s} model id") print("-" * 80) for tier, spec in RECOMMENDATIONS.items(): - print(f"{tier:14s} {spec.size_gb_q4:5.1f}GB {spec.ollama_id}") - print(f" ↳ {spec.label} — {spec.notes}") + print(f"{tier:14s} {spec.size_gb_q4:5.1f}GB {spec.model_id}") + print(f" -> {spec.label} - {spec.notes}") return 0 profile = detect() - result = ensure_recommended(profile, auto_pull=args.install) + result = ensure_recommended(profile, auto_download=args.install) if args.json: print(json.dumps(result.to_dict(), indent=2)) diff --git a/simplicio/local_models.py b/simplicio/local_models.py index 0975a6a..4d77079 100644 --- a/simplicio/local_models.py +++ b/simplicio/local_models.py @@ -1,121 +1,129 @@ -"""local_models.py — hardware-tier → local model recommendation + Ollama plumbing. +"""local_models.py - hardware-tier -> llama.cpp GGUF recommendation. Encodes the local LLM standard: - all tiers → openbmb/minicpm5:latest via local Ollama (~0.7 GB) + all tiers -> local-llama/default + bartowski/Qwen_Qwen3.5-2B-GGUF::Qwen_Qwen3.5-2B-Q6_K.gguf -The GGUF fallback is handled by simplicio.providers as -Qwen_Qwen3.5-2B-Q6_K.gguf when Ollama is unavailable. +The model runs in-process through llama-cpp-python. No Ollama daemon, pull, or +HTTP endpoint is required for the default local path. Hard rule (issue #32 follow-up): -- NEVER auto-pull a model that does not fit the detected tier. - ensure_recommended() refuses to pull if disk_gb < expected_size + - safety margin OR if the model size > hardware tier ceiling. -- Pulls require explicit opt-in (SIMPLICIO_AUTO_PULL=1 or the user passes - --auto-pull through the CLI). We tell the user the command and stop. +- NEVER auto-download a model that does not fit the detected tier. +- Downloads require explicit opt-in (SIMPLICIO_AUTO_DOWNLOAD=1 or + `simplicio doctor --install`). We tell the user the command and stop. """ from __future__ import annotations import os -import shutil -import subprocess from dataclasses import dataclass -from typing import Optional +from pathlib import Path from .hardware import HardwareProfile +from .providers import ( + LOCAL_DEFAULT_FILE as DEFAULT_LOCAL_FILE, + LOCAL_DEFAULT_REPO as DEFAULT_LOCAL_REPO, + LOCAL_EXECUTOR_DIR, + LOCAL_MODEL_PREFIX, +) + + +DEFAULT_LOCAL_MODEL_ID = f"{LOCAL_MODEL_PREFIX}default" +DEFAULT_LOCAL_LABEL = "Qwen3.5 2B Q6_K GGUF (llama.cpp)" +DEFAULT_LOCAL_SIZE_GB = 1.6 +DEFAULT_LOCAL_NOTES = ( + "canonical local doer; runs in-process with llama-cpp-python, no Ollama service" +) @dataclass class ModelSpec: tier: str - ollama_id: str + model_id: str + repo_id: str + filename: str size_gb_q4: float label: str notes: str = "" - -DEFAULT_LOCAL_OLLAMA_ID = "openbmb/minicpm5:latest" -DEFAULT_LOCAL_OLLAMA_LABEL = "MiniCPM5 local (Ollama)" -DEFAULT_LOCAL_OLLAMA_SIZE_GB = 0.7 -DEFAULT_LOCAL_OLLAMA_NOTES = ( - "canonical local primary; falls back to Qwen_Qwen3.5-2B-Q6_K.gguf in providers" -) + @property + def ollama_id(self) -> str: + """Backward-compatible read alias for older callers.""" + return self.model_id -# Order matters: list is consulted in the rare case the user asks for the -# next-step-up model. The default lookup is by exact tier. RECOMMENDATIONS: dict[str, ModelSpec] = { tier: ModelSpec( tier, - DEFAULT_LOCAL_OLLAMA_ID, - DEFAULT_LOCAL_OLLAMA_SIZE_GB, - DEFAULT_LOCAL_OLLAMA_LABEL, - DEFAULT_LOCAL_OLLAMA_NOTES, + DEFAULT_LOCAL_MODEL_ID, + DEFAULT_LOCAL_REPO, + DEFAULT_LOCAL_FILE, + DEFAULT_LOCAL_SIZE_GB, + DEFAULT_LOCAL_LABEL, + DEFAULT_LOCAL_NOTES, ) for tier in ("cpu-tiny", "cpu-small", "gpu-mid", "gpu-large", "gpu-xlarge", "unknown") } -# ---- Ollama plumbing ---- # +def local_model_dir() -> Path: + return Path(os.environ.get("SIMPLICIO_LOCAL_MODEL_DIR", LOCAL_EXECUTOR_DIR)).expanduser() -def ollama_present() -> bool: - return shutil.which("ollama") is not None +def model_file_path(spec: ModelSpec) -> Path: + override = os.environ.get("SIMPLICIO_LOCAL_MODEL_PATH") + if override: + return Path(override).expanduser() + return local_model_dir() / spec.filename -def ollama_list_installed() -> list[str]: - """Return the list of Ollama models currently installed. +def _is_gguf_file(path: Path) -> bool: + try: + with path.open("rb") as handle: + return handle.read(4) == b"GGUF" + except OSError: + return False - Each entry is the full tag the user can target (e.g. "openbmb/minicpm5:latest"). - Empty list on failure — never raises. - """ - if not ollama_present(): - return [] + +def model_file_present(spec: ModelSpec) -> bool: + return _is_gguf_file(model_file_path(spec)) + + +def is_installed(spec: ModelSpec | str) -> bool: + if isinstance(spec, ModelSpec): + return model_file_present(spec) + if spec.endswith(".gguf"): + return _is_gguf_file(local_model_dir() / spec) + if spec == DEFAULT_LOCAL_MODEL_ID: + return model_file_present(RECOMMENDATIONS["unknown"]) + return False + + +def download(spec: ModelSpec) -> tuple[bool, str]: + """Download the recommended GGUF into the executor model directory.""" try: - out = subprocess.run( - ["ollama", "list"], capture_output=True, text=True, timeout=10, + from huggingface_hub import hf_hub_download + except ImportError: + return ( + False, + "huggingface-hub not installed. Install extras: pip install 'simplicio-cli[local]'", ) - except (FileNotFoundError, subprocess.TimeoutExpired): - return [] - if out.returncode != 0: - return [] - # `ollama list` first row is the header; subsequent rows have NAME first - rows = out.stdout.strip().splitlines() - if len(rows) <= 1: - return [] - installed = [] - for line in rows[1:]: - parts = line.split() - if parts: - installed.append(parts[0]) - return installed - - -def is_installed(ollama_id: str) -> bool: - """Looser match hook kept for future aliases; currently exact tag match.""" - installed = ollama_list_installed() - return ollama_id in installed - - -def pull(ollama_id: str, timeout: int = 1800) -> tuple[bool, str]: - """Run `ollama pull `. Returns (ok, last_lines). - - Caller is responsible for tier / disk-space gating BEFORE calling this. - """ - if not ollama_present(): - return False, "ollama not on PATH" + + target_dir = local_model_dir() + target_dir.mkdir(parents=True, exist_ok=True) try: - out = subprocess.run( - ["ollama", "pull", ollama_id], - capture_output=True, text=True, timeout=timeout, + path = Path( + hf_hub_download( + repo_id=spec.repo_id, + filename=spec.filename, + local_dir=str(target_dir), + ) ) - except subprocess.TimeoutExpired: - return False, f"ollama pull timed out after {timeout}s" - log = (out.stdout or "") + (out.stderr or "") - return out.returncode == 0, log[-2000:] - - -# ---- The actual gate ---- # + except Exception as exc: # noqa: BLE001 - rendered as CLI status + return False, str(exc) + if not _is_gguf_file(path): + return False, f"{path} is not a valid GGUF file" + return True, str(path) @dataclass @@ -123,14 +131,22 @@ class RecommendationResult: spec: ModelSpec profile: HardwareProfile can_run: bool - can_pull: bool + can_download: bool installed: bool reason: str = "" + @property + def can_pull(self) -> bool: + """Backward-compatible read alias for older callers.""" + return self.can_download + def to_dict(self) -> dict: return { "tier": self.spec.tier, - "ollama_id": self.spec.ollama_id, + "model_id": self.spec.model_id, + "repo_id": self.spec.repo_id, + "filename": self.spec.filename, + "model_path": str(model_file_path(self.spec)), "label": self.spec.label, "size_gb_q4": self.spec.size_gb_q4, "notes": self.spec.notes, @@ -139,25 +155,22 @@ def to_dict(self) -> dict: "gpu": self.profile.gpu_name, "apple_silicon": self.profile.apple_silicon, "can_run": self.can_run, - "can_pull": self.can_pull, + "can_download": self.can_download, "installed": self.installed, "reason": self.reason, } -# Safety margin: refuse to pull if the detected resource isn't at least -# (size + margin) — we don't want to brick a 16 GB laptop pulling a 17.5 GB -# model and then OOM at runtime. +# Safety margin: refuse to download if the detected resource is not at least +# (size + margin). We do not want a local automation flow to fill disk/RAM with +# a model it cannot actually run. _SAFETY_MARGIN_GB = 4.0 def evaluate(profile: HardwareProfile) -> RecommendationResult: - """Pick the spec for this tier and decide whether the host can run/pull it.""" + """Pick the local GGUF spec and decide whether the host can run/download it.""" spec = RECOMMENDATIONS.get(profile.tier, RECOMMENDATIONS["unknown"]) - # `can_run`: hardware has enough headroom to actually run the model. - # On Apple Silicon, RAM == VRAM. On Linux/Windows NVIDIA, VRAM dominates; - # we still check RAM as a backstop for CPU offload. if profile.apple_silicon: usable_gb = profile.ram_gb else: @@ -165,49 +178,66 @@ def evaluate(profile: HardwareProfile) -> RecommendationResult: needed_gb = spec.size_gb_q4 + _SAFETY_MARGIN_GB can_run = usable_gb >= needed_gb - can_pull = ollama_present() and can_run + installed = is_installed(spec) + can_download = can_run and not installed reason = "" - if not ollama_present(): - reason = "ollama not installed — see https://ollama.ai" - elif not can_run: - reason = (f"detected {usable_gb:.1f} GB usable < {needed_gb:.1f} GB " - f"required for {spec.label} (size {spec.size_gb_q4:.1f} GB + " - f"{_SAFETY_MARGIN_GB:.0f} GB safety margin)") - - installed = is_installed(spec.ollama_id) if ollama_present() else False + if not can_run: + reason = ( + f"detected {usable_gb:.1f} GB usable < {needed_gb:.1f} GB " + f"required for {spec.label} (size {spec.size_gb_q4:.1f} GB + " + f"{_SAFETY_MARGIN_GB:.0f} GB safety margin)" + ) + elif not installed: + reason = f"local GGUF not installed at {model_file_path(spec)}" + return RecommendationResult( - spec=spec, profile=profile, - can_run=can_run, can_pull=can_pull, installed=installed, reason=reason, + spec=spec, + profile=profile, + can_run=can_run, + can_download=can_download, + installed=installed, + reason=reason, ) -def ensure_recommended(profile: HardwareProfile, auto_pull: bool = False) -> RecommendationResult: - """High-level orchestrator. If the recommended model isn't installed, and - auto_pull is True AND can_pull is True, run `ollama pull`. Otherwise return - a result that the CLI can render so the user knows what to do. +def ensure_recommended( + profile: HardwareProfile, + auto_download: bool = False, + *, + auto_pull: bool | None = None, +) -> RecommendationResult: + """High-level orchestrator for the default local llama.cpp model. - Honours SIMPLICIO_AUTO_PULL=1 as an alias for auto_pull=True. + If the recommended GGUF is missing and auto_download is true, download it. + Otherwise return a result the CLI can render so the user knows what to do. + `auto_pull` remains as a keyword-only alias for older code paths, but still + performs a GGUF download rather than any Ollama action. """ + if auto_pull is not None: + auto_download = auto_download or auto_pull + result = evaluate(profile) if result.installed: return result - if not result.can_pull: + if not result.can_download: return result - do_pull = auto_pull or os.environ.get("SIMPLICIO_AUTO_PULL", "").strip() in ( - "1", "true", "True", "yes", - ) - if not do_pull: - result.reason = (f"model not installed — opt in to auto-pull with " - f"`simplicio doctor --install` or " - f"`SIMPLICIO_AUTO_PULL=1 ...` " - f"(will fetch {result.spec.size_gb_q4:.1f} GB)") + do_download = auto_download or os.environ.get( + "SIMPLICIO_AUTO_DOWNLOAD", "" + ).strip() in ("1", "true", "True", "yes") + if not do_download: + result.reason = ( + "model not installed - opt in to download with " + "`simplicio doctor --install` or `SIMPLICIO_AUTO_DOWNLOAD=1 ...` " + f"(will fetch ~{result.spec.size_gb_q4:.1f} GB)" + ) return result - ok, log = pull(result.spec.ollama_id) + ok, log = download(result.spec) if ok: result.installed = True - result.reason = "pulled via ollama" + result.can_download = False + result.reason = f"downloaded GGUF to {log}" else: - result.reason = f"ollama pull failed: {log[-300:]}" + result.reason = f"GGUF download failed: {log[-300:]}" return result diff --git a/simplicio/providers.py b/simplicio/providers.py index f9e3910..bf6182b 100644 --- a/simplicio/providers.py +++ b/simplicio/providers.py @@ -21,16 +21,14 @@ given SIMPLICIO_HOOK_GUARD=1 so the inner CLI does not re-trigger the simplicio UserPromptSubmit hook (recursion guard). -4. Local Ollama default (offline-first, zero key) +4. Local llama.cpp default (offline-first, zero key) SIMPLICIO_MODEL=(unset) SIMPLICIO_BASE_URL=(unset) - -> http://localhost:11434/v1 with openbmb/minicpm5:latest - If Ollama is unavailable or the default call fails, simplicio falls back - to Path 5. + -> local-llama/default, loaded in-process with llama-cpp-python -5. In-process local inference via llama-cpp-python (GGUF fallback, zero key) +5. Explicit in-process local inference via llama-cpp-python (zero key) SIMPLICIO_MODEL=local-llama/:: -> explicit HF GGUF - SIMPLICIO_MODEL=local-llama/default -> fallback Qwen GGUF + SIMPLICIO_MODEL=local-llama/default -> default Qwen GGUF SIMPLICIO_MODEL=local-llama//abs/path/model.gguf -> direct local path The GGUF is reused from ~/.simplicio/models/executor when present, otherwise @@ -74,15 +72,12 @@ def _inline_feedback(prompt, feedback): # --------------------------------------------------------------------------- # -# Path 4: local Ollama primary + Path 5: in-process GGUF fallback. +# Path 4: local llama.cpp default + Path 5: explicit in-process GGUF. # --------------------------------------------------------------------------- # -DEFAULT_OLLAMA_MODEL = "openbmb/minicpm5:latest" -DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1" -DEFAULT_OLLAMA_API_KEY = "ollama" - -# Qwen3.5-2B Q6_K is the local GGUF fallback for machines where Ollama is not -# reachable or the MiniCPM5 call fails. +# Qwen3.5-2B Q6_K is the default local GGUF doer. It runs directly through +# llama-cpp-python, so the no-config path does not depend on Ollama or any +# HTTP server. LOCAL_DEFAULT_REPO = "bartowski/Qwen_Qwen3.5-2B-GGUF" LOCAL_DEFAULT_FILE = "Qwen_Qwen3.5-2B-Q6_K.gguf" LOCAL_EXECUTOR_DIR = "~/.simplicio/models/executor" @@ -96,22 +91,19 @@ def _inline_feedback(prompt, feedback): def _is_local(model, base): """True when generate() should route to the in-process llama backend. - Only explicit `local-llama/` models use the in-process backend. The empty - config default is the local Ollama primary and is handled separately. + Only explicit `local-llama/` models return true here. The empty config + default is handled separately so info/errors can describe the auto route. """ if model and model.startswith(LOCAL_MODEL_PREFIX): return True return False -def _is_default_ollama(model, base): - """True when the local default should use Ollama first.""" +def _is_default_local(model, base): + """True when empty config should use the in-process llama.cpp default.""" if not model and not base: return True - return ( - model == DEFAULT_OLLAMA_MODEL - and (not base or base.rstrip("/") == DEFAULT_OLLAMA_BASE_URL.rstrip("/")) - ) + return False def _local_spec(model): @@ -146,6 +138,15 @@ def _local_executor_dir() -> Path: return Path(os.environ.get("SIMPLICIO_LOCAL_MODEL_DIR", LOCAL_EXECUTOR_DIR)).expanduser() +def _is_gguf_file(path) -> bool: + """Return true when path exists and starts with the GGUF magic header.""" + try: + with Path(path).open("rb") as handle: + return handle.read(4) == b"GGUF" + except OSError: + return False + + def _local_candidates(repo, fname): """Return candidate (repo, GGUF filename) pairs in preference order.""" return [(repo, fname)] @@ -159,6 +160,11 @@ def _resolve_local_path(repo, fname, path): f"simplicio: local model not found at {path}. Point " "SIMPLICIO_LOCAL_MODEL_PATH at an existing .gguf file." ) + if not _is_gguf_file(path): + raise SystemExit( + f"simplicio: local model at {path} is not a valid GGUF file. " + "Download a GGUF model or update SIMPLICIO_LOCAL_MODEL_PATH." + ) return path try: from huggingface_hub import hf_hub_download @@ -171,9 +177,14 @@ def _resolve_local_path(repo, fname, path): for candidate_repo, candidate_file in _local_candidates(repo, fname): local_file = _local_executor_dir() / candidate_file if local_file.is_file(): - return str(local_file) + if _is_gguf_file(local_file): + return str(local_file) + errors.append(f"{local_file}: invalid GGUF header") try: - return hf_hub_download(repo_id=candidate_repo, filename=candidate_file) + downloaded = hf_hub_download(repo_id=candidate_repo, filename=candidate_file) + if _is_gguf_file(downloaded): + return downloaded + errors.append(f"{candidate_repo}/{candidate_file}: invalid GGUF header") except Exception as exc: # noqa: BLE001 - fallback to the next local GGUF errors.append(f"{candidate_repo}/{candidate_file}: {exc}") detail = "; ".join(errors) if errors else f"{fname}: unavailable" @@ -339,52 +350,6 @@ def _openai_compatible_generate(model, base, key, prompt, feedback, max_tokens): return r.choices[0].message.content -def _generate_default_ollama_with_fallback(prompt, feedback, max_tokens, cache_full_prompt): - from ._cache import CacheEntry, cache, make_key - - provider_id = f"openai-compatible:{DEFAULT_OLLAMA_BASE_URL}" - key = make_key( - provider_id, - DEFAULT_OLLAMA_MODEL, - prompt, - feedback=feedback, - max_tokens=max_tokens, - ) - cached = cache().get(key) - if cached is not None: - return cached.completion - try: - out = _openai_compatible_generate( - DEFAULT_OLLAMA_MODEL, - DEFAULT_OLLAMA_BASE_URL, - DEFAULT_OLLAMA_API_KEY, - prompt, - feedback, - max_tokens, - ) - except Exception as exc: # noqa: BLE001 - default local path falls back to GGUF - try: - return _generate_local_cached( - prompt, - feedback, - LOCAL_MODEL_PREFIX + "default", - max_tokens, - cache_full_prompt, - ) - except SystemExit as fallback_exc: - raise SystemExit( - "simplicio: default local Ollama model " - f"{DEFAULT_OLLAMA_MODEL} failed ({exc}); fallback GGUF " - f"{LOCAL_DEFAULT_FILE} also failed ({fallback_exc})" - ) - _charge_if_budgeted(DEFAULT_OLLAMA_MODEL, cache_full_prompt, out) - cache().put( - key, - CacheEntry(out, provider_id=provider_id, model=DEFAULT_OLLAMA_MODEL), - ) - return out - - def generate(prompt, feedback=None, max_tokens=4000, template_version=None): # Cache lookup BEFORE provider config. Key uses just SIMPLICIO_MODEL # (no credential check) so a hit returns without requiring an API key @@ -404,10 +369,14 @@ def generate(prompt, feedback=None, max_tokens=4000, template_version=None): c = _cfg() model = c["model"] - # Path 4: no config means local Ollama first, then GGUF fallback. - if _is_default_ollama(model, c["base"]): - return _generate_default_ollama_with_fallback( - prompt, feedback, max_tokens, cache_full_prompt + # Path 4: no config means local llama.cpp GGUF, no Ollama/HTTP service. + if _is_default_local(model, c["base"]): + return _generate_local_cached( + prompt, + feedback, + LOCAL_MODEL_PREFIX + "default", + max_tokens, + cache_full_prompt, ) # Path 5: in-process local inference via explicit `local-llama/` model. @@ -417,7 +386,7 @@ def generate(prompt, feedback=None, max_tokens=4000, template_version=None): if not model: raise SystemExit( "set SIMPLICIO_MODEL (e.g. anthropic/claude-opus-4, claude-cli/sonnet, " - "codex-cli/gpt-5, openbmb/minicpm5:latest, local-llama/default, " + "codex-cli/gpt-5, local-llama/default, " "glm-4.6, llama3, claude-opus-4-7)" ) provider_id = _provider_id(model, c["base"]) @@ -479,10 +448,12 @@ def generate(prompt, feedback=None, max_tokens=4000, template_version=None): def info(): c = _cfg() - if _is_default_ollama(c["model"], c["base"]): + if _is_default_local(c["model"], c["base"]): + repo, fname, path = _local_spec(LOCAL_MODEL_PREFIX + "default") + target = path or f"{repo}/{fname}" return ( - f"model={DEFAULT_OLLAMA_MODEL} provider=ollama " - f"base={DEFAULT_OLLAMA_BASE_URL} fallback={LOCAL_DEFAULT_FILE} " + "model=local-llama/default provider=local-llama " + f"(in-process, llama-cpp-python) target={target} " "key=not-needed" ) if _is_local(c["model"], c["base"]): diff --git a/tests/python/test_local_models.py b/tests/python/test_local_models.py index 43c5484..af06064 100644 --- a/tests/python/test_local_models.py +++ b/tests/python/test_local_models.py @@ -10,7 +10,6 @@ DEFAULT_LOCAL_MODEL_ID, ModelSpec, RECOMMENDATIONS, - RecommendationResult, evaluate, ) diff --git a/tests/python/test_package_metadata.py b/tests/python/test_package_metadata.py index 712834d..ad9491b 100644 --- a/tests/python/test_package_metadata.py +++ b/tests/python/test_package_metadata.py @@ -7,7 +7,7 @@ def test_package_version_matches_release_metadata() -> None: project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"] - assert project["version"] == "0.5.16" + assert project["version"] == "0.5.17" assert __version__ == project["version"] assert project["requires-python"] == ">=3.10" diff --git a/tests/python/test_run_cli.py b/tests/python/test_run_cli.py index 7af20dd..dba0c8e 100644 --- a/tests/python/test_run_cli.py +++ b/tests/python/test_run_cli.py @@ -91,6 +91,22 @@ def test_env_export_quotes_dotenv_values(tmp_path, monkeypatch, capsys): ) +def test_doctor_command_delegates_to_local_model_preflight(monkeypatch): + seen = {} + monkeypatch.setenv("SIMPLICIO_SKIP_AUTO_INIT", "1") + + def fake_doctor_main(argv): + seen["argv"] = argv + return 0 + + monkeypatch.setattr("simplicio.doctor.main", fake_doctor_main) + + code = cli.main(["doctor", "--json"]) + + assert code == 0 + assert seen["argv"] == ["--json"] + + def test_run_auto_task_infers_target_from_goal(tmp_path, monkeypatch, capsys): _write(tmp_path / "src" / "auth.py", "old\n") monkeypatch.setenv("SIMPLICIO_SKIP_AUTO_INIT", "1")