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.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- misterdev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Sovereign-tier orchestration capabilities.
|
|
2
|
+
|
|
3
|
+
Inspired by June 2026 arXiv research:
|
|
4
|
+
- Agentic Engineering: Code as transient resource (arXiv:2606.05608)
|
|
5
|
+
- Long-Horizon Reasoning: AB-MCTS (Sakana AI / Marlin, June 2026)
|
|
6
|
+
- Discordance-Aware Reasoning (CyberDrift, arXiv:2606.15101)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Tuple
|
|
17
|
+
|
|
18
|
+
from misterdev.logging_setup import setup_logger
|
|
19
|
+
from misterdev.llm.client import BaseLLMClient
|
|
20
|
+
from misterdev.llm.responses import extract_json_array
|
|
21
|
+
from misterdev.utils.file_utils import safe_ref_slug
|
|
22
|
+
|
|
23
|
+
logger = setup_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EphemeralCodeManager:
|
|
27
|
+
"""Manages code as a transient, instrumental resource.
|
|
28
|
+
|
|
29
|
+
In 'Agentic Engineering', code is often generated just to solve a specific
|
|
30
|
+
reasoning step and can be discarded once its goal is achieved.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_path: Path):
|
|
34
|
+
self.session_id = uuid.uuid4().hex[:8]
|
|
35
|
+
self.ephemeral_dir = (
|
|
36
|
+
project_path / ".orchestrator" / "ephemeral" / self.session_id
|
|
37
|
+
)
|
|
38
|
+
self.ephemeral_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
def run_ephemeral_script(self, code: str, name: str = "temp") -> Tuple[bool, str]:
|
|
41
|
+
"""Executes a transient script and returns its output."""
|
|
42
|
+
# Sanitize the (LLM-supplied) name: a '/' or other path char would turn
|
|
43
|
+
# the filename into a non-existent subdir and raise on write, crashing
|
|
44
|
+
# the whole build from this best-effort probe step.
|
|
45
|
+
safe = safe_ref_slug(name, fallback="temp")
|
|
46
|
+
script_path = self.ephemeral_dir / f"{safe}_{uuid.uuid4().hex[:4]}.py"
|
|
47
|
+
logger.info(f"Executing ephemeral logic: {script_path.name}")
|
|
48
|
+
try:
|
|
49
|
+
self.ephemeral_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
script_path.write_text(code, encoding="utf-8")
|
|
51
|
+
res = subprocess.run(
|
|
52
|
+
[sys.executable, str(script_path)],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=60,
|
|
56
|
+
)
|
|
57
|
+
output = res.stdout
|
|
58
|
+
if res.stderr:
|
|
59
|
+
output += f"\nERR: {res.stderr}"
|
|
60
|
+
return res.returncode == 0, output
|
|
61
|
+
except subprocess.TimeoutExpired:
|
|
62
|
+
return False, "Ephemeral script timed out after 60s"
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return False, str(e)
|
|
65
|
+
|
|
66
|
+
def cleanup(self):
|
|
67
|
+
"""Discards all transient resources from this session."""
|
|
68
|
+
if self.ephemeral_dir.exists():
|
|
69
|
+
shutil.rmtree(self.ephemeral_dir, ignore_errors=True)
|
|
70
|
+
logger.info(f"Cleaned up ephemeral session {self.session_id}")
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> "EphemeralCodeManager":
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
76
|
+
self.cleanup()
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ABMCTSPlanner:
|
|
81
|
+
"""Adaptive Branching Monte Carlo Tree Search for task planning.
|
|
82
|
+
|
|
83
|
+
Explores multiple reasoning branches to find the optimal execution path.
|
|
84
|
+
Skips branching for trivial/small tasks to save LLM calls.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, llm_client: BaseLLMClient):
|
|
88
|
+
self.llm = llm_client
|
|
89
|
+
|
|
90
|
+
def branch_and_evaluate(self, task: str, context: str, branches: int = 3) -> str:
|
|
91
|
+
"""Simulates multiple implementation paths and selects the winner.
|
|
92
|
+
|
|
93
|
+
Returns the original task unchanged if branching is not worthwhile.
|
|
94
|
+
"""
|
|
95
|
+
# Skip branching for short specs (trivial work)
|
|
96
|
+
if len(task.split()) < 50:
|
|
97
|
+
logger.info("AB-MCTS: Skipping branching (spec too short to benefit)")
|
|
98
|
+
return task
|
|
99
|
+
|
|
100
|
+
logger.info(f"AB-MCTS: Branching (n={branches})...")
|
|
101
|
+
|
|
102
|
+
simulations = []
|
|
103
|
+
for i in range(branches):
|
|
104
|
+
prompt = (
|
|
105
|
+
f"Simulation Path {i + 1}: Propose a unique implementation strategy "
|
|
106
|
+
f"for this task.\nContext: {context}\nTask: {task}"
|
|
107
|
+
)
|
|
108
|
+
strategy = self.llm.generate_code(
|
|
109
|
+
prompt, "You are a competitive systems architect."
|
|
110
|
+
)
|
|
111
|
+
simulations.append(strategy)
|
|
112
|
+
|
|
113
|
+
# Self-Evaluation / Discordance-Aware Selection
|
|
114
|
+
numbered = "\n".join(
|
|
115
|
+
f"--- PATH {i + 1} ---\n{s}" for i, s in enumerate(simulations)
|
|
116
|
+
)
|
|
117
|
+
eval_prompt = (
|
|
118
|
+
f"Evaluate these {branches} competing implementation strategies.\n"
|
|
119
|
+
f"Select the one with the highest 'Verifiability' and lowest 'Regression Risk'.\n\n"
|
|
120
|
+
f"Strategies:\n{numbered}\n\n"
|
|
121
|
+
f"Return ONLY the content of the selected strategy."
|
|
122
|
+
)
|
|
123
|
+
logger.info("AB-MCTS: Evaluating branches...")
|
|
124
|
+
return self.llm.generate_code(
|
|
125
|
+
eval_prompt, "You are a discordance-aware evaluator."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ProbeGenerator:
|
|
130
|
+
"""Synthesizes empirical fact-finding probes with high-rigor reflection.
|
|
131
|
+
|
|
132
|
+
Identifies 'assumptions' in the spec and generates scripts to verify them
|
|
133
|
+
using Python's 'inspect', 'ast', and 'dir' modules for reflective analysis.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, llm_client: BaseLLMClient):
|
|
137
|
+
self.llm = llm_client
|
|
138
|
+
|
|
139
|
+
def generate_probes(
|
|
140
|
+
self, spec: str, assessment_summary: str
|
|
141
|
+
) -> List[Dict[str, str]]:
|
|
142
|
+
"""Analyzes spec and generates a list of {name, purpose, script} probes."""
|
|
143
|
+
prompt = f"""Analyze this technical spec and project assessment.
|
|
144
|
+
Identify 1-3 critical 'assumptions' or 'unknowns' about the live codebase or environment.
|
|
145
|
+
|
|
146
|
+
Spec: {spec}
|
|
147
|
+
Assessment: {assessment_summary}
|
|
148
|
+
|
|
149
|
+
For each unknown, synthesize a transient Python 'Probe' script that uses REFLECTIVE techniques:
|
|
150
|
+
- Use 'inspect.signature()' to verify function/method arguments.
|
|
151
|
+
- Use 'inspect.getsource()' to see the actual logic.
|
|
152
|
+
- Use 'dir()' and 'getattr()' to probe runtime objects.
|
|
153
|
+
- Use 'ast.parse()' on local files to check structural definitions.
|
|
154
|
+
|
|
155
|
+
The script MUST print its findings clearly to stdout.
|
|
156
|
+
|
|
157
|
+
Return a JSON array: [{{"name": "...", "purpose": "...", "script": "..."}}]
|
|
158
|
+
Return ONLY the JSON array.
|
|
159
|
+
"""
|
|
160
|
+
logger.info("Generating empirical probes...")
|
|
161
|
+
try:
|
|
162
|
+
response = self.llm.generate_code(
|
|
163
|
+
prompt, "You are a senior empirical researcher."
|
|
164
|
+
)
|
|
165
|
+
return extract_json_array(response)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Failed to generate probes: {e}")
|
|
168
|
+
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ToolSynthesizer:
|
|
173
|
+
"""Synthesizes project-local helper tools on-the-fly."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, project_path: Path):
|
|
176
|
+
self.tools_dir = project_path / ".orchestrator" / "synthesized_tools"
|
|
177
|
+
self.tools_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
def synthesize_tool(
|
|
180
|
+
self, name: str, purpose: str, llm_client: BaseLLMClient
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Asks LLM to generate a Python script for a specific purpose."""
|
|
183
|
+
prompt = (
|
|
184
|
+
f"Synthesize a standalone Python script to serve as a local tool.\n"
|
|
185
|
+
f"Purpose: {purpose}\nTool Name: {name}\n\n"
|
|
186
|
+
f"Requirements:\n"
|
|
187
|
+
f"- Single file, standard library or project-available dependencies.\n"
|
|
188
|
+
f"- Executable as 'python tool.py'.\n"
|
|
189
|
+
f"- Return ONLY the code in a ```python code block."
|
|
190
|
+
)
|
|
191
|
+
logger.info(f"Synthesizing tool: {name}")
|
|
192
|
+
response = llm_client.generate_code(prompt, "You are a senior tools engineer.")
|
|
193
|
+
|
|
194
|
+
code = _extract_code_block(response)
|
|
195
|
+
tool_path = self.tools_dir / f"{safe_ref_slug(name, fallback='tool')}.py"
|
|
196
|
+
tool_path.write_text(code, encoding="utf-8")
|
|
197
|
+
logger.info(f"Tool {name} saved to {tool_path}")
|
|
198
|
+
return str(tool_path)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class StrategyOptimizer:
|
|
202
|
+
"""Optimizes agentic workflows via strategy simulation.
|
|
203
|
+
|
|
204
|
+
Caches strategy per task category to avoid redundant LLM calls.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
STRATEGIES = {
|
|
208
|
+
"surgical": "Search-then-Edit (Fast, minimal context)",
|
|
209
|
+
"iterative": "Standard Try-Test-Fix Loop (Thorough)",
|
|
210
|
+
"architectural": "Spec-first Redesign (High impact)",
|
|
211
|
+
"agentic": "Sovereign Agentic Engineering (Transient code, multi-agent)",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self._cache: Dict[str, str] = {}
|
|
216
|
+
|
|
217
|
+
def select_best_strategy(
|
|
218
|
+
self,
|
|
219
|
+
task_description: str,
|
|
220
|
+
task_category: str,
|
|
221
|
+
project_summary: str,
|
|
222
|
+
llm_client: BaseLLMClient,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Select strategy, using cache for repeated categories."""
|
|
225
|
+
if task_category in self._cache:
|
|
226
|
+
cached = self._cache[task_category]
|
|
227
|
+
logger.info(f"Strategy (cached for {task_category}): {cached.upper()}")
|
|
228
|
+
return cached
|
|
229
|
+
|
|
230
|
+
strategy_list = "\n".join(
|
|
231
|
+
f"{i + 1}. {k}: {v}" for i, (k, v) in enumerate(self.STRATEGIES.items())
|
|
232
|
+
)
|
|
233
|
+
prompt = (
|
|
234
|
+
f"Select the most efficient execution strategy.\n\n"
|
|
235
|
+
f"Project: {project_summary}\n"
|
|
236
|
+
f"Task category: {task_category}\n"
|
|
237
|
+
f"Task: {task_description}\n\n"
|
|
238
|
+
f"Available Strategies:\n{strategy_list}\n\n"
|
|
239
|
+
f"Return ONLY the key (surgical, iterative, architectural, or agentic)."
|
|
240
|
+
)
|
|
241
|
+
best = (
|
|
242
|
+
llm_client.generate_code(prompt, "You are a workflow optimizer.")
|
|
243
|
+
.strip()
|
|
244
|
+
.lower()
|
|
245
|
+
)
|
|
246
|
+
if best not in self.STRATEGIES:
|
|
247
|
+
best = "iterative"
|
|
248
|
+
|
|
249
|
+
self._cache[task_category] = best
|
|
250
|
+
logger.info(f"Strategy selected for {task_category}: {best.upper()}")
|
|
251
|
+
return best
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class RealTimeAligner:
|
|
255
|
+
"""Ensures multi-step alignment via a 'Shared Certified Repository' pattern."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, project_path: Path):
|
|
258
|
+
self.cert_file = project_path / ".orchestrator" / "consensus.json"
|
|
259
|
+
self.cert_file.parent.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
self._load()
|
|
261
|
+
|
|
262
|
+
def _load(self):
|
|
263
|
+
if self.cert_file.exists():
|
|
264
|
+
try:
|
|
265
|
+
self.data = json.loads(self.cert_file.read_text(encoding="utf-8"))
|
|
266
|
+
except (json.JSONDecodeError, OSError):
|
|
267
|
+
self.data = {"invariants": [], "decisions": []}
|
|
268
|
+
else:
|
|
269
|
+
self.data = {"invariants": [], "decisions": []}
|
|
270
|
+
|
|
271
|
+
def certify_decision(self, decision: str, rationale: str):
|
|
272
|
+
"""Records a certified project decision to keep future tasks aligned."""
|
|
273
|
+
self.data["decisions"].append(
|
|
274
|
+
{
|
|
275
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
276
|
+
"decision": decision,
|
|
277
|
+
"rationale": rationale,
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
self.cert_file.write_text(json.dumps(self.data, indent=2), encoding="utf-8")
|
|
281
|
+
logger.info(f"Certified Decision: {decision}")
|
|
282
|
+
|
|
283
|
+
def get_consensus_context(self) -> str:
|
|
284
|
+
"""Returns the current project consensus as a prompt string."""
|
|
285
|
+
if not self.data["decisions"]:
|
|
286
|
+
return "No prior decisions recorded."
|
|
287
|
+
|
|
288
|
+
lines = ["## Project Consensus (Certified)"]
|
|
289
|
+
for d in self.data["decisions"]:
|
|
290
|
+
lines.append(f"- {d['decision']} (Rationale: {d['rationale']})")
|
|
291
|
+
return "\n".join(lines)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _extract_code_block(response: str) -> str:
|
|
295
|
+
"""Extract the first code block from an LLM response."""
|
|
296
|
+
lines = response.split("\n")
|
|
297
|
+
in_block = False
|
|
298
|
+
code_lines = []
|
|
299
|
+
for line in lines:
|
|
300
|
+
if line.strip().startswith("```") and not in_block:
|
|
301
|
+
in_block = True
|
|
302
|
+
# Skip the opening fence line
|
|
303
|
+
continue
|
|
304
|
+
if line.strip().startswith("```") and in_block:
|
|
305
|
+
break
|
|
306
|
+
if in_block:
|
|
307
|
+
code_lines.append(line)
|
|
308
|
+
return "\n".join(code_lines) if code_lines else response.strip()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Multi-target (polyglot) gate routing.
|
|
2
|
+
|
|
3
|
+
A monorepo can hold several sub-projects in different languages — e.g. a Rust
|
|
4
|
+
core plus a TypeScript web client and a Swift app — each with its OWN build/test/
|
|
5
|
+
lint toolchain. The orchestrator otherwise assumes ONE gate for the whole repo,
|
|
6
|
+
so a task editing the web client would be (mis)gated with the Rust commands.
|
|
7
|
+
|
|
8
|
+
A ``targets`` list in project.yaml declares those sub-projects::
|
|
9
|
+
|
|
10
|
+
targets:
|
|
11
|
+
- name: core
|
|
12
|
+
path: emathy-core
|
|
13
|
+
build_command: "cargo build -p emathy-core"
|
|
14
|
+
test_command: "cargo test -p emathy-core --lib"
|
|
15
|
+
- name: web
|
|
16
|
+
path: clients/web
|
|
17
|
+
build_command: "npm run typecheck"
|
|
18
|
+
|
|
19
|
+
For each task, :func:`select_target` picks the target that owns the task's files,
|
|
20
|
+
and the executor gates that task with THAT target's commands. With no ``targets``
|
|
21
|
+
declared (or no match), everything falls back to the top-level commands, so the
|
|
22
|
+
single-target path is byte-identical to before.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
# Directories that never contain a sub-project worth gating (vendored deps, build
|
|
29
|
+
# output, VCS), so discovery skips them.
|
|
30
|
+
_SKIP_DIRS = {
|
|
31
|
+
"node_modules",
|
|
32
|
+
"target",
|
|
33
|
+
"dist",
|
|
34
|
+
"build",
|
|
35
|
+
"pkg",
|
|
36
|
+
"vendor",
|
|
37
|
+
"Pods",
|
|
38
|
+
".git",
|
|
39
|
+
".venv",
|
|
40
|
+
"venv",
|
|
41
|
+
"__pycache__",
|
|
42
|
+
".next",
|
|
43
|
+
".gradle",
|
|
44
|
+
".cargo",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# A directory holding any of these is the root of a sub-project (one toolchain).
|
|
48
|
+
_BUILD_MARKERS = (
|
|
49
|
+
"Cargo.toml",
|
|
50
|
+
"package.json",
|
|
51
|
+
"Package.swift",
|
|
52
|
+
"go.mod",
|
|
53
|
+
"meson.build",
|
|
54
|
+
"build.gradle",
|
|
55
|
+
"build.gradle.kts",
|
|
56
|
+
"pyproject.toml",
|
|
57
|
+
"CMakeLists.txt",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _has_marker(d: Path) -> bool:
|
|
62
|
+
if any((d / m).exists() for m in _BUILD_MARKERS):
|
|
63
|
+
return True
|
|
64
|
+
return any(d.glob("*.csproj")) or any(d.glob("*.sln"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _plugin_target_for(d: Path):
|
|
68
|
+
"""First registered target plugin whose markers match ``d``, or None.
|
|
69
|
+
|
|
70
|
+
A target plugin is any object exposing ``markers`` (filenames or globs) and
|
|
71
|
+
``commands(dir) -> dict`` (build/test/lint/typecheck keys), so a third party
|
|
72
|
+
can add a language/build system the built-in markers don't cover. See
|
|
73
|
+
``misterdev.plugins.TARGETS``.
|
|
74
|
+
"""
|
|
75
|
+
from misterdev.plugins import TARGETS
|
|
76
|
+
|
|
77
|
+
for name in TARGETS.names():
|
|
78
|
+
target = TARGETS.get(name)
|
|
79
|
+
for marker in getattr(target, "markers", ()):
|
|
80
|
+
if any(ch in marker for ch in "*?[") and any(d.glob(marker)):
|
|
81
|
+
return target
|
|
82
|
+
if (d / marker).exists():
|
|
83
|
+
return target
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def discover_targets(project_path: str, max_depth: int = 3) -> List[Dict[str, Any]]:
|
|
88
|
+
"""Auto-detect sub-projects (targets) in a polyglot monorepo.
|
|
89
|
+
|
|
90
|
+
Walks the tree and records the SHALLOWEST build marker in each subtree (a
|
|
91
|
+
Cargo workspace at ``rust/`` is one target, not one per crate — discovery does
|
|
92
|
+
not descend past a found marker). The repo root itself is never a target (it
|
|
93
|
+
is the top-level fallback). Build/test commands are detected per sub-project.
|
|
94
|
+
|
|
95
|
+
Conservative on purpose: returns [] unless at least TWO distinct sub-projects
|
|
96
|
+
are found, so a normal single-project repo is never turned into "targets" and
|
|
97
|
+
behavior is unchanged. Commands are best-effort — explicit ``targets`` in
|
|
98
|
+
project.yaml override and tune them.
|
|
99
|
+
"""
|
|
100
|
+
from misterdev.analyzers.project_analyzer import (
|
|
101
|
+
detect_build_command,
|
|
102
|
+
detect_test_command,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
root = Path(project_path)
|
|
106
|
+
targets: List[Dict[str, Any]] = []
|
|
107
|
+
|
|
108
|
+
def scan(d: Path, depth: int, rel: str) -> None:
|
|
109
|
+
if depth > max_depth:
|
|
110
|
+
return
|
|
111
|
+
if d.name in _SKIP_DIRS or (rel and d.name.startswith(".")):
|
|
112
|
+
return
|
|
113
|
+
plugin = _plugin_target_for(d) if rel else None
|
|
114
|
+
if rel and (_has_marker(d) or plugin is not None):
|
|
115
|
+
if plugin is not None:
|
|
116
|
+
cmds = plugin.commands(d) or {}
|
|
117
|
+
build = cmds.get("build_command")
|
|
118
|
+
test = cmds.get("test_command")
|
|
119
|
+
else:
|
|
120
|
+
cmds = {}
|
|
121
|
+
build = detect_build_command(d)
|
|
122
|
+
test = detect_test_command(d)
|
|
123
|
+
if build or test:
|
|
124
|
+
target = {
|
|
125
|
+
"name": rel.replace("/", "-"),
|
|
126
|
+
"path": rel,
|
|
127
|
+
"build_command": build,
|
|
128
|
+
"test_command": test,
|
|
129
|
+
}
|
|
130
|
+
for extra in ("lint_command", "typecheck_command"):
|
|
131
|
+
if cmds.get(extra):
|
|
132
|
+
target[extra] = cmds[extra]
|
|
133
|
+
targets.append(target)
|
|
134
|
+
return # do not descend into a found sub-project
|
|
135
|
+
try:
|
|
136
|
+
children = sorted(c for c in d.iterdir() if c.is_dir())
|
|
137
|
+
except OSError:
|
|
138
|
+
return
|
|
139
|
+
for child in children:
|
|
140
|
+
scan(child, depth + 1, f"{rel}/{child.name}" if rel else child.name)
|
|
141
|
+
|
|
142
|
+
scan(root, 0, "")
|
|
143
|
+
return targets if len(targets) >= 2 else []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _norm(path: str) -> str:
|
|
147
|
+
return (path or "").strip().strip("/")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _owns(target_path: str, file_path: str) -> bool:
|
|
151
|
+
"""True when ``file_path`` lives under ``target_path`` (or equals it)."""
|
|
152
|
+
tp = _norm(target_path)
|
|
153
|
+
fp = _norm(file_path)
|
|
154
|
+
if not tp:
|
|
155
|
+
return False
|
|
156
|
+
return fp == tp or fp.startswith(tp + "/")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def select_target(
|
|
160
|
+
targets: List[Dict[str, Any]], file_paths: List[str]
|
|
161
|
+
) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Pick the declared target that owns the most of ``file_paths``, or None.
|
|
163
|
+
|
|
164
|
+
Ties break toward the more specific (longer) target path, so a nested target
|
|
165
|
+
(``clients/web/sub``) wins over its parent. Returns None when there are no
|
|
166
|
+
targets, no files, or no target owns any file — the caller then uses the
|
|
167
|
+
top-level commands.
|
|
168
|
+
"""
|
|
169
|
+
if not targets or not file_paths:
|
|
170
|
+
return None
|
|
171
|
+
best: Optional[Dict[str, Any]] = None
|
|
172
|
+
best_count = 0
|
|
173
|
+
best_len = -1
|
|
174
|
+
for t in targets:
|
|
175
|
+
tp = _norm(t.get("path", ""))
|
|
176
|
+
if not tp:
|
|
177
|
+
continue
|
|
178
|
+
count = sum(1 for f in file_paths if _owns(tp, f))
|
|
179
|
+
if count == 0:
|
|
180
|
+
continue
|
|
181
|
+
if count > best_count or (count == best_count and len(tp) > best_len):
|
|
182
|
+
best, best_count, best_len = t, count, len(tp)
|
|
183
|
+
return best
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_GATE_KEYS = ("build_command", "test_command", "lint_command", "typecheck_command")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def target_commands(
|
|
190
|
+
target: Optional[Dict[str, Any]], config: Dict[str, Any]
|
|
191
|
+
) -> Dict[str, Optional[str]]:
|
|
192
|
+
"""Resolve the effective build/test/lint/typecheck commands for a task.
|
|
193
|
+
|
|
194
|
+
A MATCHED target is self-contained: only the commands it declares apply, and
|
|
195
|
+
any it omits are skipped (None) — NOT inherited from the top-level, which is
|
|
196
|
+
usually a different toolchain (inheriting ``cargo test`` onto a web task would
|
|
197
|
+
be meaningless). With NO matched target, the top-level commands are used
|
|
198
|
+
unchanged (the single-target path).
|
|
199
|
+
"""
|
|
200
|
+
source = target if target is not None else config
|
|
201
|
+
return {key: source.get(key) for key in _GATE_KEYS}
|
|
File without changes
|