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,95 @@
|
|
|
1
|
+
"""Pre-flight devplan validation.
|
|
2
|
+
|
|
3
|
+
Validates that a devplan is well-formed and executable before any
|
|
4
|
+
(money-spending) LLM calls are made: missing context files, dangling
|
|
5
|
+
dependency references, uninstalled test-command binaries, duplicate file
|
|
6
|
+
targets, and missing titles.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from misterdev.core.models import Task
|
|
14
|
+
from misterdev.logging_setup import setup_logger
|
|
15
|
+
|
|
16
|
+
logger = setup_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PreflightIssue:
|
|
20
|
+
"""A single problem found while validating a devplan task."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, task_id: str, severity: str, message: str):
|
|
23
|
+
self.task_id = task_id
|
|
24
|
+
self.severity = severity # "error" | "warning"
|
|
25
|
+
self.message = message
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"[{self.severity.upper()}] {self.task_id}: {self.message}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PreflightValidator:
|
|
32
|
+
"""Validates devplan tasks before execution to catch issues early."""
|
|
33
|
+
|
|
34
|
+
def validate(self, tasks: List[Task], project_path: Path) -> List[PreflightIssue]:
|
|
35
|
+
issues: List[PreflightIssue] = []
|
|
36
|
+
task_ids = {t.id for t in tasks}
|
|
37
|
+
project_path = Path(project_path)
|
|
38
|
+
|
|
39
|
+
# Map file -> tasks that modify it (to warn about conflicting targets).
|
|
40
|
+
modifiers: dict[str, list[str]] = {}
|
|
41
|
+
|
|
42
|
+
for task in tasks:
|
|
43
|
+
for f in task.context_files:
|
|
44
|
+
if not (project_path / f).exists():
|
|
45
|
+
issues.append(
|
|
46
|
+
PreflightIssue(
|
|
47
|
+
task.id, "warning", f"Context file '{f}' does not exist"
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
for dep in task.dependencies:
|
|
52
|
+
if dep not in task_ids:
|
|
53
|
+
issues.append(
|
|
54
|
+
PreflightIssue(
|
|
55
|
+
task.id,
|
|
56
|
+
"error",
|
|
57
|
+
f"Dependency '{dep}' does not match any task",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
test_cmd = task.processor_data.get("test_command")
|
|
62
|
+
if test_cmd:
|
|
63
|
+
binary = test_cmd.split()[0]
|
|
64
|
+
if not shutil.which(binary):
|
|
65
|
+
issues.append(
|
|
66
|
+
PreflightIssue(
|
|
67
|
+
task.id,
|
|
68
|
+
"warning",
|
|
69
|
+
f"Test command binary '{binary}' not found in PATH",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not task.title:
|
|
74
|
+
issues.append(PreflightIssue(task.id, "warning", "Task has no title"))
|
|
75
|
+
|
|
76
|
+
for f in task.files_to_modify:
|
|
77
|
+
modifiers.setdefault(f, []).append(task.id)
|
|
78
|
+
|
|
79
|
+
for file_path, tids in modifiers.items():
|
|
80
|
+
independent = [t for t in tasks if t.id in tids and not t.dependencies]
|
|
81
|
+
if len(tids) > 1 and len(independent) > 1:
|
|
82
|
+
issues.append(
|
|
83
|
+
PreflightIssue(
|
|
84
|
+
independent[1],
|
|
85
|
+
"warning",
|
|
86
|
+
f"File '{file_path}' modified by multiple independent tasks "
|
|
87
|
+
f"({', '.join(tids)}); they may conflict if run in the same wave",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return issues
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def has_errors(issues: List[PreflightIssue]) -> bool:
|
|
95
|
+
return any(i.severity == "error" for i in issues)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Spec-as-tests helper (opt-in, wired per-task in the executor).
|
|
2
|
+
|
|
3
|
+
Idea: before a task is implemented, turn its acceptance criteria into a failing
|
|
4
|
+
test (an executable spec), so "done" means "this test now passes" rather than a
|
|
5
|
+
model's self-report. This is the strongest possible oracle for a single task.
|
|
6
|
+
|
|
7
|
+
This module is the generation primitive (generate + extract + path). The wiring
|
|
8
|
+
lives in :meth:`MarkdownPlanExecutor._maybe_generate_spec_test` /
|
|
9
|
+
``_run_spec_test``: when ``orchestrator.spec_as_tests`` is on, the generated test
|
|
10
|
+
is written under ``.orchestrator/spec_tests/`` — deliberately OUTSIDE the
|
|
11
|
+
project's own test directory, so it is never collected by the project suite and
|
|
12
|
+
therefore can never flip the integration-gate baseline red. After the task's own
|
|
13
|
+
gates pass, it is run scoped to that one file (pytest/jest-style); a red result
|
|
14
|
+
is advisory by default and blocking under ``orchestrator.spec_as_tests_block``.
|
|
15
|
+
|
|
16
|
+
``write_spec_test``/``spec_test_path`` remain for callers that want the test in
|
|
17
|
+
the conventional location; the executor does NOT use them (it writes to the
|
|
18
|
+
baseline-safe ``.orchestrator/`` lane instead).
|
|
19
|
+
|
|
20
|
+
Everything here is best-effort and timeout-bounded: a missing client, an empty
|
|
21
|
+
criterion, or a model error yields ``None`` (no test generated), never a raise.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Callable, Optional
|
|
27
|
+
|
|
28
|
+
from misterdev.core.execution.bounded import run_bounded
|
|
29
|
+
from misterdev.logging_setup import setup_logger
|
|
30
|
+
|
|
31
|
+
logger = setup_logger(__name__)
|
|
32
|
+
|
|
33
|
+
# Generator call: takes the assembled prompt and returns the model's text.
|
|
34
|
+
# Injected in tests; defaulted to the project client's generate_code path.
|
|
35
|
+
GeneratorCall = Callable[[str], str]
|
|
36
|
+
|
|
37
|
+
_PROMPT = (
|
|
38
|
+
"You are writing a SINGLE failing test that encodes an acceptance criterion "
|
|
39
|
+
"for a feature that is NOT yet implemented. The test MUST fail today (because "
|
|
40
|
+
"the feature is absent) and pass once the feature is built correctly.\n\n"
|
|
41
|
+
"## Task\n{description}\n\n"
|
|
42
|
+
"## Acceptance Criteria\n{criteria}\n\n"
|
|
43
|
+
"## Target language\n{language}\n\n"
|
|
44
|
+
"Output ONLY the test file's source code inside a single fenced code block. "
|
|
45
|
+
"No prose, no explanation, no setup instructions."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Default per-language test-directory + filename conventions. Used only to place
|
|
49
|
+
# the generated file; callers may override the directory.
|
|
50
|
+
_LANG_TEST_DIR = {
|
|
51
|
+
"python": "tests",
|
|
52
|
+
"javascript": "tests",
|
|
53
|
+
"typescript": "tests",
|
|
54
|
+
"rust": "tests",
|
|
55
|
+
"go": ".",
|
|
56
|
+
"java": "src/test/java",
|
|
57
|
+
}
|
|
58
|
+
_LANG_EXT = {
|
|
59
|
+
"python": ".py",
|
|
60
|
+
"javascript": ".test.js",
|
|
61
|
+
"typescript": ".test.ts",
|
|
62
|
+
"rust": ".rs",
|
|
63
|
+
"go": "_test.go",
|
|
64
|
+
"java": ".java",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_FENCE = re.compile(r"```[a-zA-Z0-9_+-]*\n(.*?)```", re.DOTALL)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_spec_test(
|
|
71
|
+
task,
|
|
72
|
+
llm_client=None,
|
|
73
|
+
language: str = "python",
|
|
74
|
+
generator: Optional[GeneratorCall] = None,
|
|
75
|
+
timeout: float = 60,
|
|
76
|
+
) -> Optional[str]:
|
|
77
|
+
"""Generate a failing test source from a task's acceptance criteria.
|
|
78
|
+
|
|
79
|
+
Returns the extracted test source string, or ``None`` when there is no
|
|
80
|
+
acceptance criterion, no usable generator/client, the model returns nothing
|
|
81
|
+
parseable, or the call errors/times out. Timeout-bounded via a daemon worker
|
|
82
|
+
so a slow model can never hang a caller. Best-effort: never raises.
|
|
83
|
+
"""
|
|
84
|
+
criteria = (getattr(task, "acceptance_criteria", "") or "").strip()
|
|
85
|
+
if not criteria:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
call = generator or _default_generator(llm_client)
|
|
89
|
+
if call is None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
prompt = _PROMPT.format(
|
|
93
|
+
description=(getattr(task, "description", "") or "").strip(),
|
|
94
|
+
criteria=criteria,
|
|
95
|
+
language=language,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _work() -> Optional[str]:
|
|
99
|
+
try:
|
|
100
|
+
return extract_code(call(prompt) or "")
|
|
101
|
+
except Exception as e: # any model/IO failure is non-fatal -> no test
|
|
102
|
+
logger.debug(f"Spec-test generation unavailable: {e}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
return run_bounded(_work, timeout, None, "Spec-test generation")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def extract_code(text: str) -> Optional[str]:
|
|
109
|
+
"""Extract the source inside the first fenced code block, or None.
|
|
110
|
+
|
|
111
|
+
Falls back to the whole trimmed text when the model returned bare code with
|
|
112
|
+
no fence. Returns None for empty input.
|
|
113
|
+
"""
|
|
114
|
+
if not text or not text.strip():
|
|
115
|
+
return None
|
|
116
|
+
match = _FENCE.search(text)
|
|
117
|
+
if match:
|
|
118
|
+
body = match.group(1).strip()
|
|
119
|
+
return body or None
|
|
120
|
+
return text.strip() or None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def spec_test_path(project_root: Path, task, language: str = "python") -> Path:
|
|
124
|
+
"""Compute the path the generated spec test would be written to.
|
|
125
|
+
|
|
126
|
+
Deterministic from the task id and language; lives under the language's
|
|
127
|
+
conventional test directory. Pure (no I/O), so callers can reason about the
|
|
128
|
+
location before deciding to write.
|
|
129
|
+
"""
|
|
130
|
+
lang = (language or "python").lower()
|
|
131
|
+
test_dir = _LANG_TEST_DIR.get(lang, "tests")
|
|
132
|
+
ext = _LANG_EXT.get(lang, ".txt")
|
|
133
|
+
safe_id = re.sub(r"[^A-Za-z0-9_]", "_", str(getattr(task, "id", "task")))
|
|
134
|
+
name = f"spec_{safe_id}{ext}"
|
|
135
|
+
return project_root / test_dir / name
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def write_spec_test(
|
|
139
|
+
project_root: Path,
|
|
140
|
+
task,
|
|
141
|
+
source: str,
|
|
142
|
+
language: str = "python",
|
|
143
|
+
) -> Path:
|
|
144
|
+
"""Write ``source`` to the task's spec-test path, creating the dir.
|
|
145
|
+
|
|
146
|
+
Returns the written path. Raises OSError on a genuine write failure (the
|
|
147
|
+
caller decides whether that is fatal); generation itself is already
|
|
148
|
+
error-swallowing, so this stays strict so a wiring caller is never told a
|
|
149
|
+
file was written when it was not.
|
|
150
|
+
"""
|
|
151
|
+
path = spec_test_path(project_root, task, language)
|
|
152
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
path.write_text(source, encoding="utf-8")
|
|
154
|
+
logger.info(f"Wrote spec-as-test (expected to fail pre-implementation): {path}")
|
|
155
|
+
return path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _default_generator(llm_client) -> Optional[GeneratorCall]:
|
|
159
|
+
"""Build a generator call from the project's LLM client, or None if unusable.
|
|
160
|
+
|
|
161
|
+
Tolerant of client shape so an absent/limited client yields no generator
|
|
162
|
+
(the caller gets None) rather than raising. No network until invoked.
|
|
163
|
+
"""
|
|
164
|
+
if llm_client is None or not hasattr(llm_client, "generate_code"):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
system = (
|
|
168
|
+
"You write a single failing test that encodes one acceptance criterion. "
|
|
169
|
+
"Output only the test source inside one fenced code block."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _call(prompt: str) -> str:
|
|
173
|
+
return llm_client.generate_code(prompt, system) or ""
|
|
174
|
+
|
|
175
|
+
return _call
|