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
misterdev/config.py ADDED
@@ -0,0 +1,521 @@
1
+ import yaml
2
+ from dataclasses import asdict, dataclass, field, fields
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from misterdev.logging_setup import setup_logger
7
+
8
+ logger = setup_logger(__name__)
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Typed config schema (single source of truth).
13
+ #
14
+ # Each knob's name, type, and default live in exactly one place: a dataclass
15
+ # field. DEFAULT_CONFIG is GENERATED from these below, so the dict that the rest
16
+ # of the codebase consumes can never drift from the schema, and a default can no
17
+ # longer be copy-pasted (and diverge) across call sites. Stdlib dataclasses, no
18
+ # pydantic: config is loaded from trusted local YAML, so attribute typing +
19
+ # unknown-key validation is enough; we don't need runtime coercion.
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ @dataclass
24
+ class LLMSettings:
25
+ provider: str = "openrouter"
26
+ model: str = "anthropic/claude-sonnet-4.6"
27
+ temperature: float = 0.1
28
+ # Extra sampling parameters (top_p, top_k, min_p, repetition_penalty,
29
+ # frequency_penalty, presence_penalty, seed, ...). Each is sent only to
30
+ # models whose OpenRouter supported_parameters include it, so unsupported
31
+ # knobs never cause a 400. temperature is filtered the same way.
32
+ sampling: Dict[str, Any] = field(default_factory=dict)
33
+ api_key_env_var: str = "OPENROUTER_API_KEY"
34
+ streaming: bool = False
35
+ # Extract edits via a structured function-call (apply_edits) when the model
36
+ # supports `tools`, instead of regex-parsing markdown fences. On by default;
37
+ # falls back to markdown parsing for models without tool support.
38
+ use_tools: bool = True
39
+ failover: List[Dict[str, Any]] = field(default_factory=list)
40
+ # Per-task model routing: routing maps complexity/strategy -> tier name,
41
+ # models maps tier name -> model id (or a list of candidate ids). Empty =
42
+ # use the default model.
43
+ routing: Dict[str, Any] = field(default_factory=dict)
44
+ models: Dict[str, Any] = field(default_factory=dict)
45
+ # Ledger-driven dynamic model selection. False = off (default), True = on
46
+ # using selection_posture, "auto" = self-activating: explore cheap/free
47
+ # models on easy tasks while a (category, complexity) cell is immature, then
48
+ # settle into conservative cheap-first per cell once it has matured. Typed
49
+ # Any because it accepts a bool or the string "auto".
50
+ # escalation is the capability ladder, cheapest tier first; each tier name
51
+ # resolves through `models`. The policy uses a cheaper model on early
52
+ # attempts and climbs to the strongest tier by the final attempt.
53
+ dynamic_selection: Any = "auto"
54
+ escalation: List[str] = field(default_factory=list)
55
+ # A cheaper model is trusted for a first attempt only once it has at least
56
+ # min_observations recorded first-try attempts and a first-try success rate
57
+ # at or above first_try_floor.
58
+ min_observations: int = 5
59
+ first_try_floor: float = 0.5
60
+ # Hard-avoid a model on a (category, complexity) cell once the ledger has
61
+ # enough attempts to judge it (>= min_observations) AND its success rate on
62
+ # that cell is below this floor: it is proven unable to do this kind of task,
63
+ # so trying it only burns a failed attempt and forces escalation. Distinct
64
+ # from "unproven" (too few attempts to know). 0 disables the guard.
65
+ incompetence_floor: float = 0.2
66
+ # Skip a model on a NON-final attempt when its ledger avg latency exceeds this
67
+ # many seconds — it is proven too slow to fit the per-task wall-clock budget
68
+ # (e.g. the 5-minute cache window). Only bites once a model has recorded slow
69
+ # calls; unseen models are unaffected. 0 disables the guard (default off,
70
+ # since a sensible ceiling is project/hardware specific).
71
+ max_attempt_latency_seconds: float = 0.0
72
+ # "auto" mode treats a (category, complexity) cell as matured once it has at
73
+ # least this many recorded attempts, after which it stops exploring and
74
+ # behaves conservatively.
75
+ maturity_threshold: int = 12
76
+ # Exploration aggressiveness: "conservative" (cheap only once proven),
77
+ # "balanced" (explore cheap on low/medium-complexity first attempts), or
78
+ # "aggressive" (always try cheapest first). The final attempt is always the
79
+ # strongest tier regardless of posture.
80
+ selection_posture: str = "conservative"
81
+ # Per-complexity reasoning effort, sent only to models that support a
82
+ # reasoning budget. Default leverages reasoning where it pays off (hard
83
+ # tasks) without adding token cost to easy ones; a complexity absent from
84
+ # the map gets no reasoning. Effort values: minimal|low|medium|high|xhigh.
85
+ # medium uses "low" (not "medium"): high reasoning was the dominant per-task
86
+ # latency term (~2m47s of a 4.5m task on a medium reasoning call), and medium
87
+ # tasks rarely need deep reasoning — "low" keeps a single attempt inside the
88
+ # 5-minute cache window while preserving reasoning for genuinely large tasks.
89
+ reasoning_effort: Dict[str, str] = field(
90
+ default_factory=lambda: {"large": "high", "medium": "low"}
91
+ )
92
+ # Harvest OpenRouter's rotating free models into the cheapest tier. On by
93
+ # default for out-of-box cost savings; the quality floor (gates) plus the
94
+ # always-strong final attempt keep output safe. Set false to keep code off
95
+ # third-party free endpoints entirely.
96
+ use_free_models: bool = True
97
+ # Semantic context retrieval: rank candidate code symbols by embedding
98
+ # similarity to the task and keep the most relevant when they exceed the
99
+ # context cap, instead of truncating in arbitrary order. On by default and
100
+ # self-regulating (only embeds when selection is actually needed); degrades
101
+ # to arbitrary order if no embedding model is reachable.
102
+ semantic_retrieval: bool = True
103
+ # Which embedder backs semantic retrieval. "auto" = OpenRouter for an
104
+ # OpenRouter provider, else a local fastembed model (free, offline, no key);
105
+ # "local" forces fastembed; "openrouter" forces the API; "none" disables the
106
+ # dense signal (lexical-only ranking).
107
+ embedding_backend: str = "auto"
108
+ # fastembed model used by the local backend (downloaded once, then cached).
109
+ local_embedding_model: str = "BAAI/bge-small-en-v1.5"
110
+ # Empty = auto-pick the cheapest (free-preferred) embedding model from
111
+ # OpenRouter's embeddings catalog, like free-model harvesting; set an id to
112
+ # pin one. Ranking is forgiving, so cheapest is a fine default.
113
+ embedding_model: str = ""
114
+ # Id substrings preferred when auto-selecting among equally-priced embedding
115
+ # models; defaults to code-aware models since we rank code.
116
+ embedding_prefer: List[str] = field(default_factory=lambda: ["code"])
117
+ # Output dimensionality; 0 leaves the model default (param omitted).
118
+ embedding_dimensions: int = 0
119
+ # Weight of the lexical identifier-overlap signal vs dense cosine when
120
+ # ranking context (0 = pure dense, 1 = pure lexical). Lexical also drives
121
+ # ranking on its own when no embedding model is reachable.
122
+ lexical_weight: float = 0.3
123
+ # Whether to use models/providers that train on your inputs. Off by default:
124
+ # OpenRouter routing is constrained to providers that do not store or train
125
+ # on inputs (provider data_collection="deny"), which is what makes harvesting
126
+ # free models safe. Set true to permit training providers (more/cheaper free
127
+ # models, but your code may be used for training).
128
+ allow_training_models: bool = False
129
+ # Memoize gate-passing LLM outputs keyed by the full prompt, so an identical
130
+ # request reuses the prior result instead of calling a model. On by default:
131
+ # it is content-hashed (auto-invalidates when inputs change) and every hit is
132
+ # re-validated through the gates, so it can only save cost, never ship stale
133
+ # code.
134
+ cache: bool = True
135
+
136
+
137
+ @dataclass
138
+ class BuildSettings:
139
+ max_tasks: int = 30
140
+ max_consecutive_failures: int = 3
141
+ build_timeout: int = 120
142
+ test_timeout: int = 180
143
+ lint_timeout: int = 120
144
+ parallel_analysis: bool = True
145
+ # Global spend ceiling for a run; the master budget constraint.
146
+ budget: float = 100.0
147
+
148
+
149
+ @dataclass
150
+ class OrchestratorSettings:
151
+ max_consecutive_failures: int = 3
152
+ max_workers: int = 4
153
+ context_budget_tokens: int = 100000
154
+ # Target files at or below this many lines are sent in full; larger files
155
+ # are sent as a symbol outline plus verbatim windows of the task-relevant
156
+ # symbols, so context (and cost) scales with the edit, not the file size.
157
+ large_file_line_threshold: int = 800
158
+ max_task_attempts: int = 3
159
+ integration_gate: bool = True
160
+ # "auto" keys are budget-driven (BaseLLMClient / convergence loop) so the
161
+ # global build.budget is the single master constraint. Typed Any because
162
+ # they accept "auto", a number, or None.
163
+ max_build_iterations: Any = "auto"
164
+ certainty_threshold: float = 0.5
165
+ max_cost_per_task: Any = "auto"
166
+ allow_test_edits: bool = False
167
+ # Optional LSP semantic gate: when on, a language server checks edited files
168
+ # for errors a syntax check misses (undefined names, type errors). Off by
169
+ # default and timeout-bounded so it can never block a build; lsp_timeout
170
+ # caps how long the whole check may take before it is skipped.
171
+ lsp_diagnostics: bool = False
172
+ lsp_timeout: int = 30
173
+ # Optional runtime smoke gate: when on, the built artifact is launched,
174
+ # probed, and asserted to respond before the build is accepted. Off by
175
+ # default and timeout-bounded (runs in a daemon thread) so it can never
176
+ # block a build; missing/incomplete runtime.smoke config makes it a SKIP.
177
+ # The smoke spec itself lives under the top-level ``runtime.smoke`` key.
178
+ runtime_smoke: bool = False
179
+ # Optional web verification gate: when on, a headless browser (Playwright)
180
+ # drives the running web artifact and runs declarative checks (DOM/text
181
+ # presence, no console errors, axe accessibility, screenshot diff). Off by
182
+ # default and timeout-bounded (daemon thread) so it can never block a build;
183
+ # missing config or a missing Playwright/browser makes it a SKIP. The spec
184
+ # lives under the top-level ``runtime.web`` key.
185
+ web_verify: bool = False
186
+ # Optional vision verification gate: when on, a vision model judges whether a
187
+ # captured screenshot satisfies a stated visual requirement. Off by default
188
+ # and timeout-bounded; no config / no model / no network makes it a SKIP. The
189
+ # spec lives under the top-level ``runtime.vision`` key.
190
+ vision_verify: bool = False
191
+ # Optional mutation-score gate: when on, the project's configured mutation
192
+ # command (top-level ``mutation.command``) is run and its parsed score must
193
+ # meet ``mutation.min_score`` — proving the suite kills injected faults, not
194
+ # just passes. Off by default and timeout-bounded; no config / an unparseable
195
+ # score / a timeout is a SKIP, only a score below the floor is a RED.
196
+ mutation_gate: bool = False
197
+ verify_acceptance: bool = True
198
+ llm_acceptance_judge: bool = True
199
+ # Optional goal-completion check: when on, an LLM judge reads the goal,
200
+ # acceptance criteria, and the build's cumulative diff and reports whether the
201
+ # work actually satisfies the goal (gates green != goal met). Off by default.
202
+ # ADVISORY: it records gaps into the report and logs them but does NOT fail
203
+ # the build, unless block_on_goal_gap is also true. Timeout-bounded; no
204
+ # goal/criteria/client, an unparseable verdict, or a judge error is a SKIP.
205
+ goal_check: bool = False
206
+ block_on_goal_gap: bool = False
207
+ goal_check_timeout: int = 60
208
+ # Spec-as-tests (CONSERVATIVE, opt-in, currently DEFERRED): generate a failing
209
+ # test from a task's acceptance criteria before it is implemented. Off by
210
+ # default. The generation primitive lives in core/spec_tests.py and is tested,
211
+ # but it is NOT wired into the execute loop yet: writing a failing test inside
212
+ # the wave loop would flip the integration-gate baseline red and silently
213
+ # disable that gate, which is not control-flow-neutral. When set true today it
214
+ # only logs that the feature is staged-but-not-wired (see the seam in
215
+ # core/spec_tests.py); it never alters the build loop.
216
+ spec_as_tests: bool = False
217
+ # When spec_as_tests is on, a per-task generated spec test is run (scoped,
218
+ # from .orchestrator/spec_tests/) after the task's gates pass. ADVISORY by
219
+ # default: a still-failing spec test is logged/recorded but does not fail the
220
+ # task (the generated test may itself be imperfect). Set this true to make a
221
+ # red spec test fail acceptance and force a retry — strict TDD.
222
+ spec_as_tests_block: bool = False
223
+ # Golden suite: files the model never sees and may never edit, plus a
224
+ # blocking-gate command. Empty/None = feature off.
225
+ golden_paths: List[str] = field(default_factory=list)
226
+ golden_command: Optional[str] = None
227
+ # AB-MCTS spec refinement fires several serial LLM calls before any work;
228
+ # off by default (marginal value, large latency/cost).
229
+ enable_ab_mcts: bool = False
230
+ # Empirical probe discovery (SMART/CREATE Phase 1.5) runs an LLM call plus
231
+ # ephemeral scripts to verify facts before planning. On by default (it grounds
232
+ # the spec), but set false for a cheaper/faster run that skips the pre-work.
233
+ enable_probes: bool = True
234
+ # Auto-detect polyglot sub-projects (targets) when none are declared, so a
235
+ # monorepo gets per-toolchain gate routing with zero config. Off by default
236
+ # (explicit ``targets`` is more precise); detected commands are best-effort.
237
+ auto_targets: bool = False
238
+ # "auto" (worktree isolation on a git repo, else shared), "shared" (one
239
+ # working tree), or "worktree" (always isolate each parallel task).
240
+ parallel_mode: str = "auto"
241
+ auto_detect_dependencies: bool = False
242
+ # Optional MCP (Model Context Protocol) tool awareness: when on, the tools
243
+ # discovered from the servers in the top-level ``mcp.servers`` list are
244
+ # described to the model in the task context so it knows they exist. Off by
245
+ # default and additive only — it never changes the single-shot build loop.
246
+ # The substrate (connect/discover/call) is always available via project.mcp;
247
+ # this flag gates only the awareness injection. Timeout-bounded throughout.
248
+ mcp_enabled: bool = False
249
+ # Optional agentic MCP tool use: when on (and an MCP manager with discovered
250
+ # tools exists), a BOUNDED pre-edit loop lets the model request MCP tool
251
+ # calls to gather information; results are prepended to the task context and
252
+ # the existing edit-generation path runs unchanged. Off by default and purely
253
+ # additive — when off the executor path is byte-identical to today. Each round
254
+ # is timeout-bounded (the tool call goes through MCPManager.call_tool); the
255
+ # loop is hard-capped by ``mcp_max_tool_rounds``. Implies ``mcp_enabled`` for
256
+ # the awareness/registry, but the gathering loop is gated by this flag alone.
257
+ mcp_tool_use: bool = False
258
+ # Hard ceiling on the agentic gathering loop's rounds (see ``mcp_tool_use``).
259
+ # Each round is at most one model turn plus one tool call; the loop always
260
+ # stops at this count even if the model keeps requesting tools.
261
+ mcp_max_tool_rounds: int = 3
262
+ # Optional governance layer: when on, a risk classifier gates risky commands
263
+ # (destructive/irreversible/paid) at the command seam and an append-only
264
+ # audit trail is written. Off by default and additive — when off the command
265
+ # seam is byte-identical to today. In autonomous (non-interactive) mode a
266
+ # risky command is BLOCKED with an escalation record unless
267
+ # ``governance.auto_approve`` is set; in interactive mode it prompts. Ordinary
268
+ # build/test/lint commands classify as SAFE and always run. The policy spec
269
+ # lives under the top-level ``governance`` key.
270
+ governance: bool = False
271
+ # Bounded continuation-on-truncation for the plain text-generation path.
272
+ # When the model cuts a response off at its output-token limit (finish_reason
273
+ # "length"/"max_tokens") — which truncates a large NEW file's full content and
274
+ # fails the edit gate — issue up to this many follow-up calls asking the model
275
+ # to continue exactly where it stopped, concatenating the text before parsing.
276
+ # Activates ONLY on a truncated response, so when a response finishes normally
277
+ # the path is byte-identical to before. 0 disables it (never continues).
278
+ # Default 2: a pure-win correctness fix that costs nothing on untruncated
279
+ # responses and recovers files up to ~3x the single-shot output cap.
280
+ max_continuations: int = 2
281
+ # Optional adversarial edit critic (independent second component). When on, a
282
+ # SECOND component reviews each CANDIDATE edit before it is applied — ideally
283
+ # a DIFFERENT model (``critic.model``) so it does not inherit the generator's
284
+ # blind spots — and either approves it or returns concrete objections that are
285
+ # fed back to the generator for the next attempt. Off by default and purely
286
+ # additive: when off the executor path is byte-identical to today. Best-effort
287
+ # and timeout-bounded (daemon thread); no client, an unparseable verdict, or a
288
+ # timeout is a SKIP that lets the edit proceed to the real gates. The critic is
289
+ # advisory, never authoritative — the build/test gates remain the ground
290
+ # truth; ``critic_max_rejections`` caps how many regenerations it may force per
291
+ # task before deferring to those gates. The independent model id lives under
292
+ # the top-level ``critic.model`` key.
293
+ # True/False force it on/off; "auto" (default) enables it only for the
294
+ # cross-cutting categories where symptom-fixes, incomplete changes, and
295
+ # duplication cluster (refactor/fix/integration), so the quality review runs
296
+ # out of the box on the risky tasks without paying an extra call on every one.
297
+ adversarial_critic: Any = "auto"
298
+ critic_timeout: int = 60
299
+ critic_max_rejections: int = 2
300
+ # Task categories the critic auto-enables for when adversarial_critic="auto".
301
+ critic_auto_categories: List[str] = field(
302
+ default_factory=lambda: ["refactor", "fix", "integration"]
303
+ )
304
+ # Independent completeness-claim verifier. Before a build composes its spec
305
+ # and tasks, each feature the analyzer flagged "incomplete" and each "stub"
306
+ # file is rechecked against the REAL file + tests by a second component
307
+ # (ideally the independent ``judge.model``). A claim it refutes WITH evidence
308
+ # — documented graceful-degradation, a platform no-op, a parity shim, or
309
+ # already-tested code — is dropped, so no budget is spent "fixing" intentional
310
+ # design. Conservative: only a positive refutation drops a claim; unsure, a
311
+ # skip, an error, or a timeout KEEPS it, so genuine work is never lost. On by
312
+ # default — a cheap once-per-build correctness win that degrades to a no-op
313
+ # when no LLM client is available. ``verify_claims_timeout`` bounds each
314
+ # claim's judgment.
315
+ verify_claims: bool = True
316
+ verify_claims_timeout: int = 45
317
+
318
+
319
+ PROMPT_TEMPLATES = {
320
+ "system": (
321
+ "You are an expert developer. Follow the strategy, honor interface contracts exactly, "
322
+ "and ensure your output is syntactically valid.\n"
323
+ "{invariants}\n{consensus_context}"
324
+ ),
325
+ # Stable context (task, contracts, code) comes first and ends with
326
+ # {cache_breakpoint}; the volatile tail follows. The client caches everything
327
+ # before the breakpoint, so retries re-read it at ~10% input price. Reordering
328
+ # keeps the cached prefix identical across attempts.
329
+ "task_completion_instruction": (
330
+ "## Task\n{task.description}\n\n"
331
+ "## Acceptance Criteria\n{acceptance_criteria}\n\n"
332
+ "## Files to Edit\n{task.target_files}\n\n"
333
+ "## Interface Contracts (MUST honor exact signatures)\n{interface_contracts}\n\n"
334
+ "## Recent Changes to Related Files\n{recent_changes}\n\n"
335
+ "## Scratchpad (learnings from previous tasks)\n{scratchpad}\n\n"
336
+ "## Code Context\n{code_context}\n"
337
+ "{cache_breakpoint}\n"
338
+ "Modify the EXISTING files in scope. Do NOT create a new file that "
339
+ "re-implements functionality that already exists in the Code Context — "
340
+ "locate and edit the real file instead. Output your changes as markdown "
341
+ "code blocks with file paths."
342
+ ),
343
+ # error_logs is LAST (after the breakpoint) so the cached prefix — task,
344
+ # contracts, code — stays identical across attempts and only the changing
345
+ # error text is re-sent uncached.
346
+ "error_correction_instruction": (
347
+ "## Task\n{task.description}\n\n"
348
+ "## Interface Contracts\n{interface_contracts}\n\n"
349
+ "## Code Context\n{code_context}\n"
350
+ "{cache_breakpoint}\n"
351
+ "## Previous Attempt Failed\n{error_logs}\n\n"
352
+ "Fix the error. Output corrected code as markdown code blocks with file paths."
353
+ ),
354
+ }
355
+
356
+ # Sections whose keys are schema-validated (used for both DEFAULT_CONFIG
357
+ # generation and project.yaml typo detection).
358
+ _SECTION_SCHEMAS = {
359
+ "llm": LLMSettings,
360
+ "build": BuildSettings,
361
+ "orchestrator": OrchestratorSettings,
362
+ }
363
+
364
+ # Generated from the schemas above — do not hand-edit section contents; change
365
+ # the dataclass fields instead.
366
+ DEFAULT_CONFIG = {
367
+ "orchestrator_version": "1.0",
368
+ "llm": asdict(LLMSettings()),
369
+ "tools": [],
370
+ "environment": {"type": "none"},
371
+ # Runtime gate specs (each off unless its orchestrator flag is true and this
372
+ # carries the matching mapping): `smoke` (runtime_smoke), `web` (web_verify),
373
+ # `vision` (vision_verify). Free-form so projects can describe any
374
+ # launch/url/checks/capture/assert/timeout; not schema-validated like the
375
+ # dataclass sections, mirroring how `environment` is an open dict.
376
+ "runtime": {},
377
+ # MCP (Model Context Protocol) servers to connect to for tool discovery.
378
+ # ``servers`` is a list of {name, command, args, transport} mappings (stdio
379
+ # transport). Empty by default; awareness injection is gated separately by
380
+ # orchestrator.mcp_enabled. Open dict like ``runtime``, not schema-validated.
381
+ "mcp": {"servers": []},
382
+ # Mutation-score gate spec (off unless orchestrator.mutation_gate is true and
383
+ # this carries a ``command``): ``command`` (the mutation-testing command),
384
+ # ``min_score`` (floor, a fraction or percentage), ``timeout`` (seconds).
385
+ # Open dict like ``runtime``, not schema-validated, so any tool/command fits.
386
+ "mutation": {},
387
+ # Governance policy spec (off unless orchestrator.governance is true):
388
+ # ``approval_required`` (extra risky regex patterns), ``auto_approve`` (bool,
389
+ # let risky commands run unattended in autonomous mode), ``network``
390
+ # ("none"|"default", container egress control). Open dict like ``runtime``,
391
+ # not schema-validated, so the pattern list and knobs stay free-form.
392
+ "governance": {"network": "default"},
393
+ # Adversarial-critic spec (off unless orchestrator.adversarial_critic is
394
+ # true): ``model`` (an INDEPENDENT critic model id — different from the
395
+ # generator so it doesn't share its blind spots). Empty leaves the critic on
396
+ # the generator's own model with adversarial framing (weaker independence,
397
+ # logged). Open dict like ``runtime``, not schema-validated.
398
+ "critic": {},
399
+ # Multi-target (polyglot) gate routing. Each entry declares a sub-project
400
+ # with its own toolchain: {name, path, build_command, test_command?,
401
+ # lint_command?, typecheck_command?}. A task's gate is routed to the target
402
+ # that owns its files; with no targets (the default) the top-level commands
403
+ # are used unchanged. Open list like ``runtime``, not schema-validated.
404
+ "targets": [],
405
+ # Shared INDEPENDENT judge model for the post-gate goal-completion check and
406
+ # the LLM acceptance judge: ``model`` (an id different from the generator so
407
+ # those judgments don't share its blind spots). Empty leaves each judge on the
408
+ # generator's own model (weaker independence, logged). Open dict like
409
+ # ``runtime``, not schema-validated. (The edit-time critic has its own
410
+ # ``critic.model``; this covers the two post-implementation judges.)
411
+ "judge": {},
412
+ "prompt_templates": PROMPT_TEMPLATES,
413
+ "build": asdict(BuildSettings()),
414
+ "orchestrator": asdict(OrchestratorSettings()),
415
+ "build_command": None,
416
+ "test_command": None,
417
+ "lint_command": None,
418
+ }
419
+
420
+
421
+ _SCHEMA_FIELDS = {
422
+ section: frozenset(f.name for f in fields(schema))
423
+ for section, schema in _SECTION_SCHEMAS.items()
424
+ }
425
+
426
+
427
+ def get_section_setting(section: str, section_cfg: Dict[str, Any], key: str) -> Any:
428
+ """Like :func:`get_setting`, but for a caller that already holds the section
429
+ sub-dict (e.g. the LLM client receives only ``config["llm"]``).
430
+
431
+ Same two guarantees: single-source default (from DEFAULT_CONFIG) and a typo
432
+ in ``key`` raises instead of silently returning a default.
433
+ """
434
+ known = _SCHEMA_FIELDS.get(section)
435
+ if known is not None and key not in known:
436
+ raise KeyError(
437
+ f"Unknown config key '{section}.{key}' is not a field of "
438
+ f"{_SECTION_SCHEMAS[section].__name__}. Known: {sorted(known)}"
439
+ )
440
+ if key in (section_cfg or {}):
441
+ return section_cfg[key]
442
+ return DEFAULT_CONFIG.get(section, {}).get(key)
443
+
444
+
445
+ def get_setting(config: Dict[str, Any], section: str, key: str) -> Any:
446
+ """Read config[section][key], falling back to the canonical DEFAULT_CONFIG.
447
+
448
+ Two guarantees:
449
+ - Single source of truth for defaults: call sites pass no literal default,
450
+ so a key's default lives in exactly one place (the dataclass schema).
451
+ - Typo-safe: for a schema-validated section, an unknown ``key`` raises
452
+ immediately (caught in tests/dev) instead of silently returning a default.
453
+ This is the full-sweep typo protection without replacing the config dict.
454
+ """
455
+ return get_section_setting(section, config.get(section) or {}, key)
456
+
457
+
458
+ def warn_unknown_keys(project_config: Dict[str, Any]) -> List[str]:
459
+ """Warn about keys in a schema-validated section that the schema doesn't know.
460
+
461
+ Catches a user's typo in project.yaml (e.g. ``buildtimeout: 300``) which the
462
+ deep-merge would otherwise silently ignore. Returns the unknown keys found.
463
+ """
464
+ unknown: List[str] = []
465
+ for section, schema in _SECTION_SCHEMAS.items():
466
+ sub = project_config.get(section)
467
+ if not isinstance(sub, dict):
468
+ continue
469
+ known = {f.name for f in fields(schema)}
470
+ for key in sub:
471
+ if key not in known:
472
+ unknown.append(f"{section}.{key}")
473
+ logger.warning(
474
+ f"Unknown config key '{section}.{key}' in project.yaml is "
475
+ f"ignored (typo? known {section} keys: {sorted(known)})"
476
+ )
477
+ return unknown
478
+
479
+
480
+ class ConfigManager:
481
+ def __init__(self):
482
+ import copy
483
+
484
+ self.global_config = copy.deepcopy(DEFAULT_CONFIG)
485
+
486
+ def _deep_update(self, d: Dict[Any, Any], u: Dict[Any, Any]) -> Dict[Any, Any]:
487
+ """Deep merge two dictionaries."""
488
+ for k, v in u.items():
489
+ if isinstance(v, dict) and k in d and isinstance(d[k], dict):
490
+ d[k] = self._deep_update(d[k], v)
491
+ else:
492
+ d[k] = v
493
+ return d
494
+
495
+ def load_project_config(self, project_path: str | Path) -> Dict[str, Any]:
496
+ """Loads and merges project.yaml with global defaults."""
497
+ project_dir = Path(project_path)
498
+ yaml_path = project_dir / "project.yaml"
499
+
500
+ project_config = {}
501
+ if yaml_path.exists():
502
+ try:
503
+ with open(yaml_path, "r", encoding="utf-8") as f:
504
+ loaded_config = yaml.safe_load(f)
505
+ if isinstance(loaded_config, dict):
506
+ project_config = loaded_config
507
+ logger.info(f"Loaded project configuration from {yaml_path}")
508
+ except Exception as e:
509
+ logger.error(f"Failed to load project config at {yaml_path}: {e}")
510
+ else:
511
+ logger.warning(f"No project.yaml found at {project_dir}")
512
+
513
+ # Surface typo'd keys before they get silently ignored by the merge.
514
+ warn_unknown_keys(project_config)
515
+
516
+ # Merge defaults with project config (deep copy to avoid mutating defaults)
517
+ import copy
518
+
519
+ merged_config = copy.deepcopy(self.global_config)
520
+ merged_config = self._deep_update(merged_config, project_config)
521
+ return merged_config
File without changes
@@ -0,0 +1,88 @@
1
+ """Append-only audit trail (structured JSONL).
2
+
3
+ Records one JSON line per significant event — command run + exit, edit applied,
4
+ gate result, tool call — to ``.orchestrator/audit.jsonl`` under the project
5
+ root. Each line carries an ISO-8601 timestamp, an event ``type``, and
6
+ event-specific details.
7
+
8
+ Append-only and crash-tolerant: :meth:`AuditTrail.record` NEVER raises into the
9
+ caller. An unwritable path, a full disk, or a serialization failure is logged at
10
+ debug and dropped — audit is observability, so a logging failure must never
11
+ break a build. This mirrors the never-hang/never-hard-fail discipline of
12
+ :mod:`misterdev.core.execution.container` and ``lsp``.
13
+
14
+ Defaults ON: it is pure-win observability with no behavioral effect (it only
15
+ appends to a gitignored file under ``.orchestrator/``) and degrades silently if
16
+ the path is unwritable, so enabling it by default cannot regress a build. A
17
+ caller that wants it off passes ``enabled=False`` (or a null trail).
18
+ """
19
+
20
+ import json
21
+ import threading
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any, Optional
25
+
26
+ from misterdev.logging_setup import setup_logger
27
+
28
+ logger = setup_logger(__name__)
29
+
30
+ _AUDIT_DIRNAME = ".orchestrator"
31
+ _AUDIT_FILENAME = "audit.jsonl"
32
+
33
+
34
+ def _now_iso() -> str:
35
+ return datetime.now(timezone.utc).isoformat()
36
+
37
+
38
+ class AuditTrail:
39
+ """Append-only JSONL writer for build events.
40
+
41
+ One instance per build, rooted at the project path. Writes are guarded by a
42
+ lock so concurrent gate threads cannot interleave partial lines. Construction
43
+ never raises; the directory is created lazily on first write so an unwritable
44
+ root degrades to a no-op instead of failing at setup.
45
+ """
46
+
47
+ def __init__(self, project_path: Path, enabled: bool = True):
48
+ self.enabled = enabled
49
+ self.path = Path(project_path) / _AUDIT_DIRNAME / _AUDIT_FILENAME
50
+ self._lock = threading.Lock()
51
+
52
+ def record(self, event_type: str, **details: Any) -> None:
53
+ """Append one event line. Never raises.
54
+
55
+ ``event_type`` is the event category (``command``, ``edit``, ``gate``,
56
+ ``tool``, ``model``, ...); ``details`` are merged into the line. A
57
+ timestamp and the type are always present. Any failure (bad path,
58
+ non-serializable detail, I/O error) is logged at debug and swallowed.
59
+ """
60
+ if not self.enabled:
61
+ return
62
+ entry = {"ts": _now_iso(), "type": event_type}
63
+ # Merge details, coercing anything non-serializable to a string so a
64
+ # surprising value can never make the whole line unwritable.
65
+ for key, value in details.items():
66
+ try:
67
+ json.dumps(value)
68
+ entry[key] = value
69
+ except (TypeError, ValueError):
70
+ entry[key] = str(value)
71
+ try:
72
+ line = json.dumps(entry, ensure_ascii=False)
73
+ except (TypeError, ValueError) as e:
74
+ logger.debug(f"Audit entry not serializable, dropped: {e}")
75
+ return
76
+ try:
77
+ with self._lock:
78
+ self.path.parent.mkdir(parents=True, exist_ok=True)
79
+ with open(self.path, "a", encoding="utf-8") as fh:
80
+ fh.write(line + "\n")
81
+ except OSError as e:
82
+ logger.debug(f"Audit write failed (dropped): {e}")
83
+
84
+ def record_command(self, command: str, ok: bool, cwd: Optional[str] = None) -> None:
85
+ self.record("command", command=command, ok=ok, cwd=cwd)
86
+
87
+ def record_edit(self, path: str, action: str = "apply") -> None:
88
+ self.record("edit", path=path, action=action)
@@ -0,0 +1,10 @@
1
+ """Compatibility re-export of the package-level configuration.
2
+
3
+ The canonical configuration lives in ``misterdev.config``. This
4
+ module exposes the same ``DEFAULT_CONFIG`` and ``ConfigManager`` under the
5
+ ``core`` namespace so callers can import from either location.
6
+ """
7
+
8
+ from misterdev.config import DEFAULT_CONFIG, ConfigManager
9
+
10
+ __all__ = ["DEFAULT_CONFIG", "ConfigManager"]
File without changes