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,208 @@
1
+ """misterdev as an MCP server: drive the orchestrator from any MCP client.
2
+
3
+ This is a THIN adapter over the same ``ProjectOrchestrator`` the CLI uses. The
4
+ heavy work — reading the codebase, symbol-graph context management, multi-step
5
+ reasoning, model selection, budget — all runs IN THIS PROCESS with misterdev's
6
+ own LLM key. The client only sends a short instruction and receives a short
7
+ summary, so the codebase never enters the client's context window and the
8
+ context-scaling misterdev exists to provide is fully preserved.
9
+
10
+ Run it with the ``misterdev-mcp`` entry point (needs the ``mcp`` extra:
11
+ ``pip install 'misterdev[mcp]'``). Tool definitions carry per-parameter
12
+ descriptions and honest behavioral annotations (read-only vs. destructive,
13
+ idempotent, open-world) so an AI agent can pick and call them correctly.
14
+ """
15
+
16
+ from typing import Annotated, Any, Dict, Optional
17
+
18
+ from mcp.server.fastmcp import FastMCP
19
+ from mcp.types import ToolAnnotations
20
+ from pydantic import Field
21
+
22
+ from misterdev.agent import ProjectOrchestrator
23
+
24
+ # Conservative default $ ceiling for an AI-client-triggered build (the CLI
25
+ # default is higher). The client can raise it explicitly per call.
26
+ _DEFAULT_MCP_BUDGET = 10.0
27
+
28
+ mcp = FastMCP("misterdev")
29
+
30
+
31
+ @mcp.tool(
32
+ annotations=ToolAnnotations(
33
+ title="List registered projects",
34
+ readOnlyHint=True,
35
+ idempotentHint=True,
36
+ openWorldHint=False,
37
+ )
38
+ )
39
+ def list_projects() -> Dict[str, Any]:
40
+ """List every project misterdev currently knows about.
41
+
42
+ Use this first to see what is registered before calling ``status`` or
43
+ ``build`` on a specific one. Read-only — nothing is changed.
44
+
45
+ Returns a mapping of project id to its registered path and name.
46
+ """
47
+ return ProjectOrchestrator().list_projects()
48
+
49
+
50
+ @mcp.tool(
51
+ annotations=ToolAnnotations(
52
+ title="Project status",
53
+ readOnlyHint=True,
54
+ idempotentHint=True,
55
+ openWorldHint=False,
56
+ )
57
+ )
58
+ def status(
59
+ path: Annotated[
60
+ str,
61
+ Field(
62
+ description="Path to the project (its directory, containing project.yaml)."
63
+ ),
64
+ ],
65
+ ) -> Dict[str, Any]:
66
+ """Show a project's tasks and their current state.
67
+
68
+ Use to inspect what work exists and how far it has progressed, e.g. before
69
+ deciding whether to ``run`` pending tasks or ``build`` something new.
70
+ Read-only — it does not run anything.
71
+
72
+ Returns the project's tasks with their statuses.
73
+ """
74
+ return ProjectOrchestrator().get_project_status(path)
75
+
76
+
77
+ @mcp.tool(
78
+ annotations=ToolAnnotations(
79
+ title="Scan for projects",
80
+ readOnlyHint=False,
81
+ destructiveHint=False,
82
+ idempotentHint=True,
83
+ openWorldHint=False,
84
+ )
85
+ )
86
+ def scan(
87
+ directory: Annotated[
88
+ str,
89
+ Field(description="Directory to search recursively for projects to register."),
90
+ ],
91
+ ) -> str:
92
+ """Discover misterdev projects under a directory and register them.
93
+
94
+ Use once to make projects known to misterdev before inspecting or building
95
+ them. It writes to the project registry but does not touch project code, and
96
+ re-scanning the same directory is idempotent.
97
+
98
+ Returns a short confirmation string.
99
+ """
100
+ ProjectOrchestrator().scan_directory(directory)
101
+ return f"Scanned and registered projects under: {directory}"
102
+
103
+
104
+ @mcp.tool(
105
+ annotations=ToolAnnotations(
106
+ title="Autonomous build",
107
+ readOnlyHint=False,
108
+ destructiveHint=True,
109
+ idempotentHint=False,
110
+ openWorldHint=True,
111
+ )
112
+ )
113
+ def build(
114
+ path: Annotated[
115
+ str, Field(description="Path to the project to build (its directory).")
116
+ ],
117
+ goal: Annotated[
118
+ str,
119
+ Field(
120
+ description="Plain-English goal, or a mode word: 'debug', 'complete', or 'review'."
121
+ ),
122
+ ],
123
+ budget: Annotated[
124
+ float,
125
+ Field(description="Maximum dollars to spend on this run.", gt=0),
126
+ ] = _DEFAULT_MCP_BUDGET,
127
+ dry_run: Annotated[
128
+ bool,
129
+ Field(description="Plan and preview tasks without editing any code."),
130
+ ] = False,
131
+ parallel: Annotated[
132
+ bool,
133
+ Field(
134
+ description="Run independent tasks concurrently in isolated git worktrees."
135
+ ),
136
+ ] = False,
137
+ max_tasks: Annotated[
138
+ Optional[int],
139
+ Field(description="Cap how many tasks are planned/executed (bounds cost)."),
140
+ ] = None,
141
+ ) -> str:
142
+ """Autonomously plan AND execute a goal in a project, from scratch.
143
+
144
+ This is the main tool. Unlike ``run`` (which executes an existing plan),
145
+ ``build`` analyzes the project, decomposes ``goal`` into tasks, edits the
146
+ code, and verifies each change through build/test/lint/typecheck gates,
147
+ reverting anything that regresses.
148
+
149
+ DESTRUCTIVE: it edits files and makes git commits. It refuses to run on a
150
+ dirty working tree (commit or stash first). Use ``dry_run=True`` to preview
151
+ the plan without changing anything. ``budget`` caps spend; ``max_tasks``
152
+ caps scope. It calls an external LLM provider (open-world).
153
+
154
+ Returns a compact report: what was done, gate results, and cost.
155
+ """
156
+ orch = ProjectOrchestrator()
157
+ parts = [goal, "--budget", str(budget)]
158
+ if dry_run:
159
+ parts.append("--dry-run")
160
+ if parallel:
161
+ parts.append("--parallel")
162
+ if max_tasks is not None:
163
+ parts += ["--max-tasks", str(max_tasks)]
164
+ report = orch.build(path, " ".join(parts))
165
+ outcome = "succeeded" if orch.last_build_succeeded else "did not fully succeed"
166
+ return f"Build {outcome}.\n\n{report}"
167
+
168
+
169
+ @mcp.tool(
170
+ annotations=ToolAnnotations(
171
+ title="Run planned tasks",
172
+ readOnlyHint=False,
173
+ destructiveHint=True,
174
+ idempotentHint=False,
175
+ openWorldHint=True,
176
+ )
177
+ )
178
+ def run(
179
+ path: Annotated[str, Field(description="Path to the project.")],
180
+ task_id: Annotated[
181
+ Optional[str],
182
+ Field(description="Run only this task id; omit to run all pending tasks."),
183
+ ] = None,
184
+ dry_run: Annotated[
185
+ bool, Field(description="Preview the tasks without executing them.")
186
+ ] = False,
187
+ ) -> str:
188
+ """Execute a project's ALREADY-PLANNED pending tasks (a devplan).
189
+
190
+ Use this when tasks already exist (from a prior ``plan`` or devplan) and you
191
+ just want to run them — it does NOT analyze or decompose a goal; that is
192
+ ``build``'s job. Pass ``task_id`` to run a single task.
193
+
194
+ DESTRUCTIVE: it edits files and commits. Use ``dry_run=True`` to preview.
195
+ Calls an external LLM provider (open-world). Returns a summary.
196
+ """
197
+ orch = ProjectOrchestrator()
198
+ if task_id:
199
+ orch.run_task(path, task_id)
200
+ return f"Ran task {task_id} for {path}."
201
+ orch.run_project(path, dry_run=dry_run)
202
+ verb = "Previewed" if dry_run else "Ran"
203
+ return f"{verb} pending tasks for {path}."
204
+
205
+
206
+ def main() -> None:
207
+ """Console entry point: serve misterdev over stdio MCP."""
208
+ mcp.run()
misterdev/nl_cli.py ADDED
@@ -0,0 +1,149 @@
1
+ """Natural-language CLI: describe what you want in English; misterdev routes it.
2
+
3
+ When ``misterdev`` is invoked with something that isn't a known subcommand, the
4
+ whole request is treated as plain English. misterdev's own model maps it to an
5
+ action, shows a preview, asks for confirmation before anything mutating, and
6
+ runs it — so there are no flags to memorize. Known subcommands still work
7
+ unchanged for power users.
8
+ """
9
+
10
+ from typing import Any, Dict
11
+
12
+ from rich.console import Console
13
+
14
+ from misterdev.config import ConfigManager
15
+ from misterdev.llm.client import create_llm_client
16
+ from misterdev.llm.responses import extract_json_object
17
+ from misterdev.logging_setup import setup_logger
18
+
19
+ logger = setup_logger(__name__)
20
+ console = Console()
21
+
22
+ # Commands the natural-language layer can resolve to. Kept in sync with the CLI.
23
+ KNOWN_COMMANDS = {"scan", "list", "status", "report", "run", "plan", "build"}
24
+ _MUTATING = {"scan", "run", "build"}
25
+
26
+ _SYSTEM = (
27
+ "You translate a developer's plain-English request into a single misterdev "
28
+ "action. misterdev is an autonomous build orchestrator. Reply with ONLY a "
29
+ "JSON object, no prose."
30
+ )
31
+
32
+ _PROMPT = """Map this request to one misterdev action.
33
+
34
+ Commands:
35
+ - build: autonomously build / fix / complete a coding GOAL in a project (edits code).
36
+ - run: run a project's already-planned pending tasks.
37
+ - scan: discover and register projects under a directory.
38
+ - status: show a project's tasks and state.
39
+ - report: summarize the last build's cost and results.
40
+ - list: list registered projects.
41
+ - plan: analyze a project and propose work.
42
+
43
+ Return ONLY this JSON (omit keys you don't need):
44
+ {{"command": "<one of build|run|scan|status|report|list|plan>",
45
+ "path": "<project or directory path, default '.'>",
46
+ "goal": "<the plain-English goal, for build>",
47
+ "budget": <dollars as number or null>,
48
+ "dry_run": <true|false>,
49
+ "parallel": <true|false>,
50
+ "max_tasks": <int or null>}}
51
+
52
+ Request: {request}"""
53
+
54
+
55
+ def parse_intent(request: str, client) -> Dict[str, Any]:
56
+ """One model call: request -> a structured action dict (empty on failure)."""
57
+ text = client.generate_code(_PROMPT.format(request=request), _SYSTEM)
58
+ obj = extract_json_object(text or "")
59
+ return obj if isinstance(obj, dict) else {}
60
+
61
+
62
+ def _build_args(intent: Dict[str, Any]) -> str:
63
+ """Render a build intent into the flag string ``ProjectOrchestrator.build`` parses."""
64
+ parts = [str(intent.get("goal") or "").strip()]
65
+ budget = intent.get("budget")
66
+ if isinstance(budget, (int, float)):
67
+ parts += ["--budget", str(budget)]
68
+ if intent.get("dry_run"):
69
+ parts.append("--dry-run")
70
+ if intent.get("parallel"):
71
+ parts.append("--parallel")
72
+ mt = intent.get("max_tasks")
73
+ if isinstance(mt, int):
74
+ parts += ["--max-tasks", str(mt)]
75
+ return " ".join(p for p in parts if p)
76
+
77
+
78
+ def preview(intent: Dict[str, Any]) -> str:
79
+ """A human-readable one-liner of the resolved command."""
80
+ cmd = intent.get("command")
81
+ path = intent.get("path") or "."
82
+ if cmd == "build":
83
+ return f"build {path} {_build_args(intent)}".strip()
84
+ if cmd == "run":
85
+ return f"run {path}" + (" --dry-run" if intent.get("dry_run") else "")
86
+ if cmd == "scan":
87
+ return f"scan {path}"
88
+ return f"{cmd} {path}".strip()
89
+
90
+
91
+ def _dispatch(intent: Dict[str, Any], orchestrator) -> int:
92
+ cmd = intent.get("command")
93
+ path = intent.get("path") or "."
94
+ if cmd == "build":
95
+ report = orchestrator.build(path, _build_args(intent))
96
+ console.print(report)
97
+ return 0 if orchestrator.last_build_succeeded else 1
98
+ if cmd == "run":
99
+ orchestrator.run_project(path, dry_run=bool(intent.get("dry_run")))
100
+ return 0
101
+ if cmd == "scan":
102
+ orchestrator.scan_directory(path)
103
+ console.print("[green]Scan complete.[/]")
104
+ return 0
105
+ if cmd == "status":
106
+ console.print(orchestrator.get_project_status(path))
107
+ return 0
108
+ if cmd == "list":
109
+ console.print(orchestrator.list_projects())
110
+ return 0
111
+ if cmd == "plan":
112
+ orchestrator.interactive_plan(path, "")
113
+ return 0
114
+ if cmd == "report":
115
+ console.print(orchestrator.get_project_status(path))
116
+ return 0
117
+ return 1
118
+
119
+
120
+ def route(request: str, orchestrator, confirm=input) -> int:
121
+ """Resolve a plain-English request to an action and run it.
122
+
123
+ Returns a process exit code. ``confirm`` is injectable for testing.
124
+ """
125
+ cfg = ConfigManager().load_project_config(".")
126
+ try:
127
+ client = create_llm_client(cfg)
128
+ except Exception as e:
129
+ console.print(
130
+ "[yellow]Natural-language mode needs an LLM configured "
131
+ f"(model + API key). {e}[/]\nRun `misterdev --help` for the flag-based CLI."
132
+ )
133
+ return 1
134
+
135
+ intent = parse_intent(request, client)
136
+ cmd = intent.get("command")
137
+ if cmd not in KNOWN_COMMANDS:
138
+ console.print(
139
+ "[yellow]Couldn't map that to an action.[/] Try `misterdev --help`."
140
+ )
141
+ return 1
142
+
143
+ console.print(f"[dim]→ I'll run:[/] misterdev {preview(intent)}")
144
+ if cmd in _MUTATING:
145
+ answer = confirm("proceed? [Y/n] ").strip().lower()
146
+ if answer and answer not in ("y", "yes"):
147
+ console.print("Cancelled.")
148
+ return 0
149
+ return _dispatch(intent, orchestrator)
misterdev/plugins.py ADDED
@@ -0,0 +1,115 @@
1
+ """Extensibility registry for tools, gates, and targets.
2
+
3
+ Built-in capabilities self-register here, and third-party packages contribute
4
+ through ``importlib.metadata`` entry points, so ``pip install misterdev-plugin-x``
5
+ adds a tool/gate/target with zero edits to this codebase:
6
+
7
+ # in the plugin package's pyproject.toml
8
+ [project.entry-points."misterdev.tools"]
9
+ my_tool = "my_pkg:MyTool"
10
+
11
+ Entry points are discovered lazily on first lookup and never override a built-in
12
+ of the same name (a plugin cannot hijack ``command``); a plugin that fails to
13
+ import is logged and skipped, never fatal. Registration is thread-safe because
14
+ the orchestrator resolves capabilities from parallel executor workers.
15
+ """
16
+
17
+ import importlib.metadata
18
+ import threading
19
+ from typing import Callable, Dict, Generic, List, Optional, TypeVar, Union
20
+
21
+ from misterdev.logging_setup import setup_logger
22
+
23
+ logger = setup_logger(__name__)
24
+
25
+ T = TypeVar("T")
26
+
27
+
28
+ def _iter_entry_points(group: str):
29
+ """Entry points for ``group`` across Python 3.10–3.13 metadata APIs."""
30
+ eps = importlib.metadata.entry_points()
31
+ if hasattr(eps, "select"): # 3.10+ selectable interface
32
+ return eps.select(group=group)
33
+ return eps.get(group, []) # pragma: no cover - legacy dict interface (<3.10)
34
+
35
+
36
+ class Registry(Generic[T]):
37
+ """A name -> item map that also discovers external items via entry points.
38
+
39
+ ``item`` is whatever the kind needs — for tools/gates it is a class or
40
+ factory; the registry does not construct it, so a caller stays in control of
41
+ how the object is instantiated.
42
+ """
43
+
44
+ def __init__(self, kind: str, entry_point_group: str):
45
+ self.kind = kind
46
+ self._group = entry_point_group
47
+ self._items: Dict[str, T] = {}
48
+ self._lock = threading.Lock()
49
+ self._entry_points_loaded = False
50
+
51
+ def register(
52
+ self, name: str, item: Optional[T] = None
53
+ ) -> Union[T, Callable[[T], T]]:
54
+ """Register ``item`` under ``name``.
55
+
56
+ Usable directly (``TOOLS.register("command", CommandTool)``) or as a
57
+ decorator (``@TOOLS.register("command")``). A later registration of the
58
+ same name replaces the earlier one, so an in-process override is explicit.
59
+ """
60
+
61
+ def _add(obj: T) -> T:
62
+ with self._lock:
63
+ self._items[name] = obj
64
+ return obj
65
+
66
+ return _add(item) if item is not None else _add
67
+
68
+ def unregister(self, name: str) -> None:
69
+ """Remove a registration if present (a no-op otherwise). Lets a plugin be
70
+ disabled at runtime and keeps tests from leaking registrations."""
71
+ with self._lock:
72
+ self._items.pop(name, None)
73
+
74
+ def get(self, name: str) -> Optional[T]:
75
+ """Look up an item by name (loading external plugins first), or None."""
76
+ self._ensure_entry_points()
77
+ with self._lock:
78
+ return self._items.get(name)
79
+
80
+ def names(self) -> List[str]:
81
+ """All registered names, built-in and plugin, sorted."""
82
+ self._ensure_entry_points()
83
+ with self._lock:
84
+ return sorted(self._items)
85
+
86
+ def _ensure_entry_points(self) -> None:
87
+ if self._entry_points_loaded:
88
+ return
89
+ with self._lock:
90
+ if self._entry_points_loaded:
91
+ return
92
+ self._entry_points_loaded = True
93
+ for ep in _iter_entry_points(self._group):
94
+ if ep.name in self._items:
95
+ # A built-in already claims this name; a plugin must not
96
+ # silently shadow it.
97
+ logger.debug(
98
+ f"{self.kind} plugin {ep.name!r} shadows a built-in; ignored"
99
+ )
100
+ continue
101
+ try:
102
+ self._items[ep.name] = ep.load()
103
+ logger.info(f"Loaded {self.kind} plugin: {ep.name}")
104
+ except Exception as e:
105
+ # A broken third-party plugin must never break the build.
106
+ logger.warning(
107
+ f"Skipping unloadable {self.kind} plugin {ep.name!r}: {e}"
108
+ )
109
+
110
+
111
+ # One registry per extensible kind. The entry-point group is the third-party
112
+ # contract; keep these strings stable.
113
+ TOOLS: Registry = Registry("tool", "misterdev.tools")
114
+ GATES: Registry = Registry("gate", "misterdev.gates")
115
+ TARGETS: Registry = Registry("target", "misterdev.targets")
misterdev/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+ from misterdev.core.models import Task, ExecutionResult
3
+ from misterdev.core.execution.project import Project
4
+
5
+
6
+ class BaseTaskExecutor(ABC):
7
+ @abstractmethod
8
+ def execute(self, task: Task, project_context: Project) -> ExecutionResult:
9
+ """Executes a given task within the project context."""
10
+ pass
@@ -0,0 +1,90 @@
1
+ """Markdown plan executor - executes tasks via LLM with Try-Test-Fix loop.
2
+
3
+ This package preserves the original ``markdown_plan_executor`` module's import
4
+ path: the executor class and every module-level helper it historically exported
5
+ are re-exported here unchanged. The implementation is split into cohesive
6
+ mixins purely as code movement; behaviour is identical.
7
+ """
8
+
9
+ from typing import Optional
10
+
11
+ from misterdev.core.context.scratchpad import Scratchpad
12
+ from misterdev.core.verification.validator import StallDetector
13
+ from misterdev.task_executors.base_executor import BaseTaskExecutor
14
+
15
+ from .helpers import (
16
+ logger,
17
+ _is_golden_path,
18
+ _LANG_MAP,
19
+ EDIT_FORMAT_INSTRUCTIONS,
20
+ FULL_FILE_FALLBACK_INSTRUCTIONS,
21
+ JUDGE_MIN_BUDGET_FRACTION,
22
+ _relevant_line_ranges,
23
+ _merge_ranges,
24
+ _window_lines,
25
+ _bisect_first_failing,
26
+ _is_truncated,
27
+ _detect_language,
28
+ _is_test_file,
29
+ _extract_acceptance_command,
30
+ _test_metrics,
31
+ _count_tautologies,
32
+ _diagnose_tampering,
33
+ _diagnose_py_tampering,
34
+ )
35
+ from .execute_mixin import ExecuteMixin
36
+ from .git_mixin import GitMixin
37
+ from .commands_mixin import CommandsMixin
38
+ from .context_mixin import ContextMixin
39
+ from .llm_mixin import LLMMixin
40
+ from .gates_mixin import GatesMixin
41
+ from .critic_spec_mixin import CriticSpecMixin
42
+ from .edits_mixin import EditsMixin
43
+ from .results_mixin import ResultsMixin
44
+
45
+ # Re-exports preserving the original module's public surface (see module docstring).
46
+ __all__ = [
47
+ "MarkdownPlanExecutor",
48
+ "logger",
49
+ "_is_golden_path",
50
+ "_LANG_MAP",
51
+ "EDIT_FORMAT_INSTRUCTIONS",
52
+ "FULL_FILE_FALLBACK_INSTRUCTIONS",
53
+ "JUDGE_MIN_BUDGET_FRACTION",
54
+ "_relevant_line_ranges",
55
+ "_merge_ranges",
56
+ "_window_lines",
57
+ "_bisect_first_failing",
58
+ "_is_truncated",
59
+ "_detect_language",
60
+ "_is_test_file",
61
+ "_extract_acceptance_command",
62
+ "_test_metrics",
63
+ "_count_tautologies",
64
+ "_diagnose_tampering",
65
+ "_diagnose_py_tampering",
66
+ ]
67
+
68
+
69
+ class MarkdownPlanExecutor(
70
+ ExecuteMixin,
71
+ GitMixin,
72
+ CommandsMixin,
73
+ ContextMixin,
74
+ LLMMixin,
75
+ GatesMixin,
76
+ CriticSpecMixin,
77
+ EditsMixin,
78
+ ResultsMixin,
79
+ BaseTaskExecutor,
80
+ ):
81
+ """Executes tasks with a Try-Test-Fix loop.
82
+
83
+ Uses git branch-per-task for atomic execution: each task runs on a
84
+ temporary branch. Success merges to the current branch. Failure
85
+ deletes the branch, leaving the repo clean.
86
+ """
87
+
88
+ def __init__(self, scratchpad: Optional[Scratchpad] = None):
89
+ self.scratchpad = scratchpad or Scratchpad()
90
+ self.stall_detector = StallDetector()
@@ -0,0 +1,82 @@
1
+ """Command execution and file snapshot operations."""
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+ from misterdev.core.models import Task
6
+ from misterdev.core.execution.project import Project
7
+ from misterdev.core.verification.validator import _run_cmd
8
+ from misterdev.utils.file_utils import write_file
9
+
10
+ from .helpers import logger
11
+
12
+
13
+ class CommandsMixin:
14
+ # ----------------------------------------------------------------
15
+ # Command execution and file operations
16
+ # ----------------------------------------------------------------
17
+
18
+ def _run_command(
19
+ self, project: Project, command: str, timeout: int = 120, cwd=None
20
+ ) -> tuple:
21
+ # cwd lets a routed multi-target task run its gate in the TARGET's
22
+ # directory (e.g. `npm run typecheck` under clients/web), not the repo
23
+ # root where that command would not resolve. Defaults to project.path.
24
+ run_dir = cwd or project.path
25
+ logger.info(f"Running: {command} (cwd={run_dir}, timeout={timeout}s)")
26
+ activation = (
27
+ project.env_manager.activate_command() if project.env_manager else None
28
+ )
29
+ # Governance gate + audit trail (both no-ops when off: governance_policy
30
+ # is None unless orchestrator.governance is set; audit only appends).
31
+ # getattr-guarded so a lightweight project stub without these subsystems
32
+ # still executes commands unchanged.
33
+ return _run_cmd(
34
+ command,
35
+ run_dir,
36
+ activation,
37
+ timeout,
38
+ policy=getattr(project, "governance_policy", None),
39
+ audit=getattr(project, "audit_trail", None),
40
+ )
41
+
42
+ def _snapshot_files(
43
+ self, project: Project, files: List[str]
44
+ ) -> Dict[str, Optional[str]]:
45
+ snapshot = {}
46
+ for file_path in files:
47
+ full_path = project.path / file_path
48
+ if full_path.exists():
49
+ try:
50
+ snapshot[file_path] = full_path.read_text(encoding="utf-8")
51
+ except (UnicodeDecodeError, OSError):
52
+ snapshot[file_path] = None
53
+ else:
54
+ snapshot[file_path] = None
55
+ return snapshot
56
+
57
+ def _revert_files(
58
+ self, project: Project, snapshot: Dict[str, Optional[str]]
59
+ ) -> None:
60
+ for file_path, content in snapshot.items():
61
+ full_path = project.path / file_path
62
+ if content is None:
63
+ if full_path.exists():
64
+ full_path.unlink()
65
+ else:
66
+ write_file(full_path, content)
67
+
68
+ def _record_success(self, task: Task, files: List[str]) -> None:
69
+ self.scratchpad.record(
70
+ category="pattern",
71
+ discovery=f"Task {task.id} completed successfully",
72
+ task_id=task.id,
73
+ files=files,
74
+ tags=[task.category],
75
+ )
76
+
77
+ def _get_processor_config(self, project: Project) -> dict:
78
+ processors = project.config.get("task_processors", [])
79
+ for p in processors:
80
+ if p.get("type") == "markdown_planner":
81
+ return p.get("settings", {})
82
+ return {}