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,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