misterdev 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. misterdev/__init__.py +3 -0
  2. misterdev/agent.py +2166 -0
  3. misterdev/agent_helpers.py +194 -0
  4. misterdev/analyzers/__init__.py +0 -0
  5. misterdev/analyzers/project_analyzer/__init__.py +246 -0
  6. misterdev/analyzers/project_analyzer/detection.py +146 -0
  7. misterdev/analyzers/project_analyzer/merge.py +137 -0
  8. misterdev/analyzers/project_analyzer/overview.py +312 -0
  9. misterdev/analyzers/project_analyzer/prompts.py +83 -0
  10. misterdev/cli.py +370 -0
  11. misterdev/config.py +521 -0
  12. misterdev/core/__init__.py +0 -0
  13. misterdev/core/audit.py +88 -0
  14. misterdev/core/config.py +10 -0
  15. misterdev/core/context/__init__.py +0 -0
  16. misterdev/core/context/change_tracker.py +218 -0
  17. misterdev/core/context/contracts/__init__.py +63 -0
  18. misterdev/core/context/contracts/_log.py +9 -0
  19. misterdev/core/context/contracts/_text.py +12 -0
  20. misterdev/core/context/contracts/extraction.py +30 -0
  21. misterdev/core/context/contracts/python_generic.py +42 -0
  22. misterdev/core/context/contracts/registry.py +127 -0
  23. misterdev/core/context/contracts/rust_line.py +225 -0
  24. misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
  25. misterdev/core/context/lsp.py +174 -0
  26. misterdev/core/context/scratchpad.py +111 -0
  27. misterdev/core/context/topography/__init__.py +43 -0
  28. misterdev/core/context/topography/_log.py +10 -0
  29. misterdev/core/context/topography/cache.py +91 -0
  30. misterdev/core/context/topography/engine.py +172 -0
  31. misterdev/core/context/topography/graph.py +675 -0
  32. misterdev/core/context/topography/nodes.py +55 -0
  33. misterdev/core/context/topography/parsers.py +95 -0
  34. misterdev/core/context/topography/syntax.py +54 -0
  35. misterdev/core/economics/__init__.py +0 -0
  36. misterdev/core/economics/context_budget.py +175 -0
  37. misterdev/core/economics/embeddings.py +232 -0
  38. misterdev/core/economics/free_models.py +108 -0
  39. misterdev/core/economics/llm_cache.py +105 -0
  40. misterdev/core/economics/model_catalog.py +79 -0
  41. misterdev/core/economics/model_ledger.py +331 -0
  42. misterdev/core/economics/model_selector.py +281 -0
  43. misterdev/core/execution/__init__.py +0 -0
  44. misterdev/core/execution/bounded.py +50 -0
  45. misterdev/core/execution/container.py +221 -0
  46. misterdev/core/execution/error_classifier.py +366 -0
  47. misterdev/core/execution/error_resolver.py +201 -0
  48. misterdev/core/execution/governance.py +283 -0
  49. misterdev/core/execution/outcomes.py +50 -0
  50. misterdev/core/execution/progress.py +120 -0
  51. misterdev/core/execution/project.py +231 -0
  52. misterdev/core/execution/registry.py +97 -0
  53. misterdev/core/execution/runtime.py +279 -0
  54. misterdev/core/gitcmd.py +39 -0
  55. misterdev/core/integration/__init__.py +0 -0
  56. misterdev/core/integration/mcp.py +368 -0
  57. misterdev/core/integration/mcp_gather.py +186 -0
  58. misterdev/core/models.py +35 -0
  59. misterdev/core/modes.py +184 -0
  60. misterdev/core/planning/__init__.py +0 -0
  61. misterdev/core/planning/advisor.py +89 -0
  62. misterdev/core/planning/assessment.py +135 -0
  63. misterdev/core/planning/decomposer.py +387 -0
  64. misterdev/core/planning/metacognition.py +103 -0
  65. misterdev/core/planning/sovereign.py +308 -0
  66. misterdev/core/planning/targets.py +201 -0
  67. misterdev/core/reporting/__init__.py +0 -0
  68. misterdev/core/reporting/report.py +377 -0
  69. misterdev/core/reporting/report_view.py +151 -0
  70. misterdev/core/task.py +163 -0
  71. misterdev/core/verification/__init__.py +0 -0
  72. misterdev/core/verification/claim_verifier.py +210 -0
  73. misterdev/core/verification/critic.py +324 -0
  74. misterdev/core/verification/gatekeeper/__init__.py +631 -0
  75. misterdev/core/verification/gatekeeper/constants.py +138 -0
  76. misterdev/core/verification/gatekeeper/helpers.py +28 -0
  77. misterdev/core/verification/goal_check.py +219 -0
  78. misterdev/core/verification/independent.py +68 -0
  79. misterdev/core/verification/mutation_gate.py +221 -0
  80. misterdev/core/verification/preflight.py +95 -0
  81. misterdev/core/verification/spec_tests.py +175 -0
  82. misterdev/core/verification/validator.py +495 -0
  83. misterdev/core/verification/vision_verify.py +185 -0
  84. misterdev/core/verification/web_verify.py +408 -0
  85. misterdev/environments/__init__.py +0 -0
  86. misterdev/environments/base_env.py +18 -0
  87. misterdev/environments/container_env.py +87 -0
  88. misterdev/environments/venv_env.py +42 -0
  89. misterdev/llm/__init__.py +0 -0
  90. misterdev/llm/client/__init__.py +152 -0
  91. misterdev/llm/client/base.py +382 -0
  92. misterdev/llm/client/edits.py +70 -0
  93. misterdev/llm/client/embeddings.py +121 -0
  94. misterdev/llm/client/errors.py +134 -0
  95. misterdev/llm/client/providers.py +535 -0
  96. misterdev/llm/client/response.py +24 -0
  97. misterdev/llm/prompt_manager.py +82 -0
  98. misterdev/llm/responses/__init__.py +34 -0
  99. misterdev/llm/responses/apply.py +131 -0
  100. misterdev/llm/responses/json_extract.py +80 -0
  101. misterdev/llm/responses/models.py +43 -0
  102. misterdev/llm/responses/parsing.py +494 -0
  103. misterdev/logging_setup.py +20 -0
  104. misterdev/mcp_server.py +208 -0
  105. misterdev/nl_cli.py +149 -0
  106. misterdev/plugins.py +115 -0
  107. misterdev/py.typed +0 -0
  108. misterdev/task_executors/__init__.py +0 -0
  109. misterdev/task_executors/base_executor.py +10 -0
  110. misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
  111. misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
  112. misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
  113. misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
  114. misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
  115. misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
  116. misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
  117. misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
  118. misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
  119. misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
  120. misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
  121. misterdev/tools/__init__.py +19 -0
  122. misterdev/tools/base_tool.py +14 -0
  123. misterdev/tools/command.py +75 -0
  124. misterdev/tools/file_io.py +69 -0
  125. misterdev/tools/formatter.py +26 -0
  126. misterdev/tools/git_tool.py +90 -0
  127. misterdev/utils/__init__.py +0 -0
  128. misterdev/utils/file_utils.py +169 -0
  129. misterdev/utils/process.py +23 -0
  130. misterdev-0.2.0.dist-info/METADATA +326 -0
  131. misterdev-0.2.0.dist-info/RECORD +136 -0
  132. misterdev-0.2.0.dist-info/WHEEL +5 -0
  133. misterdev-0.2.0.dist-info/entry_points.txt +3 -0
  134. misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
  135. misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
  136. misterdev-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,121 @@
1
+ import importlib.util
2
+ from typing import List
3
+
4
+ from misterdev.config import get_section_setting, get_setting
5
+ from misterdev.logging_setup import setup_logger
6
+
7
+ from .providers import _deny_unless_training_allowed, _openrouter_sdk
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class OpenRouterEmbeddingClient:
13
+ """Embedding client over OpenRouter's OpenAI-compatible embeddings endpoint."""
14
+
15
+ def __init__(self, config: dict, model: str):
16
+ llm_config = config.get("llm", {})
17
+ self.client, self.api_key = _openrouter_sdk(llm_config)
18
+ self.model = model
19
+ self.dimensions = get_section_setting("llm", llm_config, "embedding_dimensions")
20
+ self.data_collection = _deny_unless_training_allowed(llm_config)
21
+
22
+ def embed(self, texts: List[str]) -> List[List[float]]:
23
+ """Return one vector per input text, in input order."""
24
+ kwargs = {
25
+ "model": self.model,
26
+ "input": texts,
27
+ "extra_body": {"provider": {"data_collection": self.data_collection}},
28
+ }
29
+ if self.dimensions:
30
+ kwargs["dimensions"] = self.dimensions
31
+ response = self.client.embeddings.create(**kwargs)
32
+ ordered = sorted(response.data, key=lambda d: d.index)
33
+ return [list(d.embedding) for d in ordered]
34
+
35
+
36
+ class LocalEmbeddingClient:
37
+ """Embedding client backed by a local fastembed model (offline, no API key).
38
+
39
+ The model is downloaded once and cached by fastembed, then runs on CPU via
40
+ ONNX. Loading is lazy (first ``embed`` call) so merely constructing the
41
+ client is cheap and failure (e.g. fastembed not installed) surfaces where
42
+ the factory can fall back gracefully.
43
+ """
44
+
45
+ def __init__(self, config: dict):
46
+ llm_config = config.get("llm", {})
47
+ self.model = (
48
+ get_section_setting("llm", llm_config, "local_embedding_model")
49
+ or "BAAI/bge-small-en-v1.5"
50
+ )
51
+ self._embedder = None
52
+
53
+ def _ensure(self):
54
+ if self._embedder is None:
55
+ from fastembed import TextEmbedding
56
+
57
+ self._embedder = TextEmbedding(model_name=self.model)
58
+ return self._embedder
59
+
60
+ def embed(self, texts: List[str]) -> List[List[float]]:
61
+ """Return one vector per input text, in input order."""
62
+ embedder = self._ensure()
63
+ return [list(vec) for vec in embedder.embed(list(texts))]
64
+
65
+
66
+ def _create_local_embedding_client(config: dict):
67
+ try:
68
+ return LocalEmbeddingClient(config)
69
+ except Exception as e:
70
+ logger.warning(f"Local embedding client unavailable: {e}")
71
+ return None
72
+
73
+
74
+ def _create_openrouter_embedding_client(config: dict):
75
+ if get_setting(config, "llm", "provider") != "openrouter":
76
+ return None
77
+ from misterdev.core.economics.embeddings import pick_embedding_model
78
+
79
+ model = pick_embedding_model(
80
+ get_setting(config, "llm", "embedding_model"),
81
+ prefer=get_setting(config, "llm", "embedding_prefer"),
82
+ )
83
+ if not model:
84
+ return None
85
+ try:
86
+ return OpenRouterEmbeddingClient(config, model)
87
+ except Exception as e:
88
+ logger.warning(f"Embedding client unavailable: {e}")
89
+ return None
90
+
91
+
92
+ def create_embedding_client(config: dict):
93
+ """Build an embedding client, or None when embeddings are unavailable.
94
+
95
+ Backend (llm.embedding_backend): "local" forces fastembed; "openrouter"
96
+ forces the API; "none" disables dense ranking; "auto" prefers a local
97
+ fastembed model when the extra is installed (free, offline, never blocked by
98
+ a provider data policy) and falls back to the OpenRouter API embedder only
99
+ when local isn't available. Any setup failure returns None, so retrieval
100
+ degrades to lexical-only rather than breaking.
101
+ """
102
+ backend = get_setting(config, "llm", "embedding_backend")
103
+ if backend == "none":
104
+ return None
105
+ if backend == "local":
106
+ return _create_local_embedding_client(config)
107
+ if backend == "openrouter":
108
+ return _create_openrouter_embedding_client(config)
109
+ # auto
110
+ if get_setting(config, "llm", "provider") == "openrouter":
111
+ # Prefer a LOCAL fastembed model when the extra is installed: it is free,
112
+ # offline, and — unlike an OpenRouter embedding model under
113
+ # data_collection=deny — never blocked by a provider data policy (that
114
+ # block silently dropped both emathy runs to lexical-only ranking). When
115
+ # fastembed isn't installed, keep the API embedder (no regression).
116
+ if importlib.util.find_spec("fastembed") is not None:
117
+ local = _create_local_embedding_client(config)
118
+ if local is not None:
119
+ return local
120
+ return _create_openrouter_embedding_client(config)
121
+ return _create_local_embedding_client(config)
@@ -0,0 +1,134 @@
1
+ from typing import Optional
2
+
3
+
4
+ class BudgetExceededError(Exception):
5
+ pass
6
+
7
+
8
+ class LLMCallError(Exception):
9
+ def __init__(self, message: str, retryable: bool = False):
10
+ super().__init__(message)
11
+ self.retryable = retryable
12
+
13
+
14
+ # HTTP status codes that denote a transient failure worth retrying / failing
15
+ # over. The 4xx members are the standard retryable ones (request timeout,
16
+ # conflict, too-early, rate limit); any other 4xx is a hard client error.
17
+ RETRYABLE_STATUS_CODES = frozenset({408, 409, 425, 429, 500, 502, 503, 504, 529})
18
+
19
+ # Exception class names (across the openai / anthropic / httpx SDKs) that always
20
+ # denote a transient failure, regardless of how the message happens to be
21
+ # phrased. Matched by bare class name so neither SDK must be importable here.
22
+ RETRYABLE_EXCEPTION_NAMES = frozenset(
23
+ {
24
+ "RateLimitError",
25
+ "APITimeoutError",
26
+ "APIConnectionError",
27
+ "APIConnectionTimeoutError",
28
+ "InternalServerError",
29
+ "ServiceUnavailableError",
30
+ "Timeout",
31
+ "TimeoutError",
32
+ "ConnectionError",
33
+ "ConnectTimeout",
34
+ "ReadTimeout",
35
+ }
36
+ )
37
+
38
+ # Last-resort substring signals for plain exceptions that carry neither a status
39
+ # code nor a recognizable type (e.g. a provider that raises RuntimeError with a
40
+ # descriptive message). Checked only after the structured signals above.
41
+ RETRYABLE_ERROR_MARKERS = (
42
+ "rate limit",
43
+ "too many requests",
44
+ "timeout",
45
+ "timed out",
46
+ "temporarily unavailable",
47
+ "overloaded",
48
+ "connection",
49
+ "try again",
50
+ "429",
51
+ "500",
52
+ "502",
53
+ "503",
54
+ "504",
55
+ "529",
56
+ )
57
+
58
+
59
+ def _error_status_code(error: Exception) -> Optional[int]:
60
+ """Best-effort HTTP status code from a provider exception, or None.
61
+
62
+ Covers the openai/anthropic ``status_code`` attribute and the httpx-style
63
+ nested ``response.status_code``. ``bool`` is excluded so a stray ``True``
64
+ isn't read as code 1.
65
+ """
66
+ for attr in ("status_code", "http_status"):
67
+ val = getattr(error, attr, None)
68
+ if isinstance(val, bool):
69
+ continue
70
+ if isinstance(val, int):
71
+ return val
72
+ if isinstance(val, str) and val.isdigit():
73
+ return int(val)
74
+ resp = getattr(error, "response", None)
75
+ sc = getattr(resp, "status_code", None)
76
+ if isinstance(sc, int) and not isinstance(sc, bool):
77
+ return sc
78
+ return None
79
+
80
+
81
+ def _is_retryable_error(error: Exception) -> bool:
82
+ """Classify an exception as transient (retry / fail over) or terminal.
83
+
84
+ Structured signals win: an explicit HTTP status code or a known transient
85
+ exception type is authoritative, so a hard 4xx (e.g. 400/401/404) is never
86
+ retried even if its message text coincidentally contains a retryable marker.
87
+ Only when no structured signal is present do we fall back to substrings.
88
+ """
89
+ code = _error_status_code(error)
90
+ if code is not None:
91
+ if code in RETRYABLE_STATUS_CODES:
92
+ return True
93
+ if 400 <= code < 500:
94
+ return False
95
+ if code >= 500:
96
+ return True
97
+ if type(error).__name__ in RETRYABLE_EXCEPTION_NAMES:
98
+ return True
99
+ text = str(error).lower()
100
+ return any(marker in text for marker in RETRYABLE_ERROR_MARKERS)
101
+
102
+
103
+ def _is_out_of_credits(error: Exception) -> bool:
104
+ """True when a provider rejects the call for lack of funds (HTTP 402).
105
+
106
+ This is terminal and account-wide — retrying or failing over to another
107
+ model cannot fix it — so it is surfaced as budget exhaustion, not a generic
108
+ call error. Detected by the 402 status code, with a message fallback for
109
+ providers that only carry it in the text.
110
+ """
111
+ if _error_status_code(error) == 402:
112
+ return True
113
+ text = str(error).lower()
114
+ return "insufficient credit" in text or (
115
+ "402" in text and ("credit" in text or "payment" in text)
116
+ )
117
+
118
+
119
+ def _api_error(provider: str, error: Exception) -> Exception:
120
+ """Wrap a provider exception for the caller to raise.
121
+
122
+ An out-of-credits (402) response becomes a ``BudgetExceededError`` so the
123
+ run halts gracefully through the existing budget-halt path with an
124
+ actionable message, instead of crashing with a stack trace. Everything else
125
+ is an ``LLMCallError`` with retryability classified.
126
+ """
127
+ if _is_out_of_credits(error):
128
+ return BudgetExceededError(
129
+ f"{provider} out of credits (HTTP 402): add credits to continue "
130
+ "(https://openrouter.ai/settings/credits)"
131
+ )
132
+ return LLMCallError(
133
+ f"{provider} API error: {error}", retryable=_is_retryable_error(error)
134
+ )